Skip to content

Commit 590e4c7

Browse files
authored
Allow More Fonts/Fontnames for Exact Width Calculation (#3326)
* Allow More Fonts/Fontnames for Exact Width Calculation Fix #3190. A limited set of explicitly-named font files can be used when an exact width calculation is required. User noted that font files are named differently on Mac than on Windows (if the fonts are installed on Linux, the font names probably match Windows). Since the algorithm for generating the Mac file name from the font name seems easy, that algorithm is invoked when the Windows-named file is not found. Moving on from there, it seems odd that only a small set of fonts are supported. It is, of course, impossible to support all possible fonts out of the box. However, it is possible to allow the user to supply additional mappings from font name to file name (or override existing mappings if neither the Windows nor Mac name matches the user's system), and doing so is permitted with this change: ```php \PhpOffice\PhpSpreadsheet\Shared\Font::setExtraFontArray([ 'fontname' => [ /* More than 1 can be specified */ 'x' => 'fontfilenamefornormal.ttf', 'xb' => 'fontfilenameforbold.ttf', 'xi' => 'fontfilenameforitalic.ttf', 'xbi' => 'fontfilenameforbolditalic.ttf', ], ]); ``` * Unexpected Test Difference ... between Windows and Linux.
1 parent 393514d commit 590e4c7

32 files changed

+199
-7
lines changed

src/PhpSpreadsheet/Shared/Font.php

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,23 @@ class Font
200200
],
201201
];
202202

203+
/**
204+
* Array that can be used to supplement FONT_FILE_NAMES for calculating exact width.
205+
*
206+
* @var array
207+
*/
208+
private static $extraFontArray = [];
209+
210+
public static function setExtraFontArray(array $extraFontArray): void
211+
{
212+
self::$extraFontArray = $extraFontArray;
213+
}
214+
215+
public static function getExtraFontArray(): array
216+
{
217+
return self::$extraFontArray;
218+
}
219+
203220
/**
204221
* AutoSize method.
205222
*
@@ -403,10 +420,6 @@ public static function calculateColumnWidth(
403420
*/
404421
public static function getTextWidthPixelsExact(string $text, FontStyle $font, int $rotation = 0): int
405422
{
406-
if (!function_exists('imagettfbbox')) {
407-
throw new PhpSpreadsheetException('GD library needs to be enabled');
408-
}
409-
410423
// font size should really be supplied in pixels in GD2,
411424
// but since GD2 seems to assume 72dpi, pixels and points are the same
412425
$fontFile = self::getTrueTypeFontFileFromFont($font);
@@ -532,7 +545,8 @@ public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkP
532545
}
533546

534547
$name = $font->getName();
535-
if (!isset(self::FONT_FILE_NAMES[$name])) {
548+
$fontArray = array_merge(self::FONT_FILE_NAMES, self::$extraFontArray);
549+
if (!isset($fontArray[$name])) {
536550
throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file');
537551
}
538552
$bold = $font->getBold();
@@ -544,7 +558,7 @@ public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkP
544558
if ($italic) {
545559
$index .= 'i';
546560
}
547-
$fontFile = self::FONT_FILE_NAMES[$name][$index];
561+
$fontFile = $fontArray[$name][$index];
548562

549563
$separator = '';
550564
if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') {
@@ -554,7 +568,31 @@ public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkP
554568

555569
// Check if file actually exists
556570
if ($checkPath && !file_exists($fontFile)) {
557-
throw new PhpSpreadsheetException('TrueType Font file not found');
571+
$alternateName = $name;
572+
if ($index !== 'x' && $fontArray[$name][$index] !== $fontArray[$name]['x']) {
573+
// Bold but no italic:
574+
// Comic Sans
575+
// Tahoma
576+
// Neither bold nor italic:
577+
// Impact
578+
// Lucida Console
579+
// Lucida Sans Unicode
580+
// Microsoft Sans Serif
581+
// Symbol
582+
if ($index === 'xb') {
583+
$alternateName .= ' Bold';
584+
} elseif ($index === 'xi') {
585+
$alternateName .= ' Italic';
586+
} elseif ($fontArray[$name]['xb'] === $fontArray[$name]['xbi']) {
587+
$alternateName .= ' Bold';
588+
} else {
589+
$alternateName .= ' Bold Italic';
590+
}
591+
}
592+
$fontFile = self::$trueTypeFontPath . $separator . $alternateName . '.ttf';
593+
if (!file_exists($fontFile)) {
594+
throw new PhpSpreadsheetException('TrueType Font file not found');
595+
}
558596
}
559597

560598
return $fontFile;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Shared;
4+
5+
use PhpOffice\PhpSpreadsheet\Exception as SSException;
6+
use PhpOffice\PhpSpreadsheet\Shared\Font;
7+
use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class FontFileNameTest extends TestCase
11+
{
12+
private const DEFAULT_DIRECTORY = 'tests/data/Shared/FakeFonts/Default';
13+
private const MAC_DIRECTORY = 'tests/data/Shared/FakeFonts/Mac';
14+
15+
/** @var string */
16+
private $holdDirectory;
17+
18+
/** @var array */
19+
private $holdExtraFontArray;
20+
21+
protected function setUp(): void
22+
{
23+
$this->holdDirectory = Font::getTrueTypeFontPath();
24+
$this->holdExtraFontArray = Font::getExtraFontArray();
25+
Font::setExtraFontArray([
26+
'Extra Font' => [
27+
'x' => 'extrafont.ttf',
28+
'xb' => 'extrafontbd.ttf',
29+
'xi' => 'extrafonti.ttf',
30+
'xbi' => 'extrafontbi.ttf',
31+
],
32+
]);
33+
}
34+
35+
protected function tearDown(): void
36+
{
37+
Font::setTrueTypeFontPath($this->holdDirectory);
38+
Font::setExtraFontArray($this->holdExtraFontArray);
39+
}
40+
41+
/**
42+
* @dataProvider providerDefault
43+
*/
44+
public function testDefaultFilenames(string $expected, array $fontArray): void
45+
{
46+
if ($expected === 'exception') {
47+
$this->expectException(SSException::class);
48+
$this->expectExceptionMessage('TrueType Font file not found');
49+
}
50+
Font::setTrueTypeFontPath(self::DEFAULT_DIRECTORY);
51+
$font = (new StyleFont())->applyFromArray($fontArray);
52+
$result = Font::getTrueTypeFontFileFromFont($font);
53+
self::assertSame($expected, basename($result));
54+
}
55+
56+
public function providerDefault(): array
57+
{
58+
return [
59+
['arial.ttf', ['name' => 'Arial']],
60+
['arialbd.ttf', ['name' => 'Arial', 'bold' => true]],
61+
['ariali.ttf', ['name' => 'Arial', 'italic' => true]],
62+
['arialbi.ttf', ['name' => 'Arial', 'bold' => true, 'italic' => true]],
63+
['cour.ttf', ['name' => 'Courier New']],
64+
['courbd.ttf', ['name' => 'Courier New', 'bold' => true]],
65+
['couri.ttf', ['name' => 'Courier New', 'italic' => true]],
66+
['courbi.ttf', ['name' => 'Courier New', 'bold' => true, 'italic' => true]],
67+
['impact.ttf', ['name' => 'Impact']],
68+
'no bold impact' => ['impact.ttf', ['name' => 'Impact', 'bold' => true]],
69+
'no italic impact' => ['impact.ttf', ['name' => 'Impact', 'italic' => true]],
70+
'no bold italic impact' => ['impact.ttf', ['name' => 'Impact', 'bold' => true, 'italic' => true]],
71+
['tahoma.ttf', ['name' => 'Tahoma']],
72+
['tahomabd.ttf', ['name' => 'Tahoma', 'bold' => true]],
73+
'no italic tahoma' => ['tahoma.ttf', ['name' => 'Tahoma', 'italic' => true]],
74+
'no bold italic tahoma' => ['tahomabd.ttf', ['name' => 'Tahoma', 'bold' => true, 'italic' => true]],
75+
'Times New Roman not in directory for this test' => ['exception', ['name' => 'Times New Roman']],
76+
['extrafont.ttf', ['name' => 'Extra Font']],
77+
['extrafontbd.ttf', ['name' => 'Extra Font', 'bold' => true]],
78+
['extrafonti.ttf', ['name' => 'Extra Font', 'italic' => true]],
79+
['extrafontbi.ttf', ['name' => 'Extra Font', 'bold' => true, 'italic' => true]],
80+
];
81+
}
82+
83+
/**
84+
* @dataProvider providerMac
85+
*/
86+
public function testMacFilenames(string $expected, array $fontArray): void
87+
{
88+
if ($expected === 'exception') {
89+
$this->expectException(SSException::class);
90+
$this->expectExceptionMessage('TrueType Font file not found');
91+
}
92+
Font::setTrueTypeFontPath(self::MAC_DIRECTORY);
93+
$font = (new StyleFont())->applyFromArray($fontArray);
94+
$result = Font::getTrueTypeFontFileFromFont($font);
95+
self::assertSame($expected, ucfirst(basename($result))); // allow for Windows case-insensitivity
96+
}
97+
98+
public function providerMac(): array
99+
{
100+
return [
101+
['Arial.ttf', ['name' => 'Arial']],
102+
['Arial Bold.ttf', ['name' => 'Arial', 'bold' => true]],
103+
['Arial Italic.ttf', ['name' => 'Arial', 'italic' => true]],
104+
['Arial Bold Italic.ttf', ['name' => 'Arial', 'bold' => true, 'italic' => true]],
105+
['Courier New.ttf', ['name' => 'Courier New']],
106+
['Courier New Bold.ttf', ['name' => 'Courier New', 'bold' => true]],
107+
['Courier New Italic.ttf', ['name' => 'Courier New', 'italic' => true]],
108+
['Courier New Bold Italic.ttf', ['name' => 'Courier New', 'bold' => true, 'italic' => true]],
109+
['Impact.ttf', ['name' => 'Impact']],
110+
'no bold impact' => ['Impact.ttf', ['name' => 'Impact', 'bold' => true]],
111+
'no italic impact' => ['Impact.ttf', ['name' => 'Impact', 'italic' => true]],
112+
'no bold italic impact' => ['Impact.ttf', ['name' => 'Impact', 'bold' => true, 'italic' => true]],
113+
['Tahoma.ttf', ['name' => 'Tahoma']],
114+
['Tahoma Bold.ttf', ['name' => 'Tahoma', 'bold' => true]],
115+
'no italic tahoma' => ['Tahoma.ttf', ['name' => 'Tahoma', 'italic' => true]],
116+
'no bold italic tahoma' => ['Tahoma Bold.ttf', ['name' => 'Tahoma', 'bold' => true, 'italic' => true]],
117+
'Times New Roman not in directory for this test' => ['exception', ['name' => 'Times New Roman']],
118+
['Extra Font.ttf', ['name' => 'Extra Font']],
119+
['Extra Font Bold.ttf', ['name' => 'Extra Font', 'bold' => true]],
120+
['Extra Font Italic.ttf', ['name' => 'Extra Font', 'italic' => true]],
121+
['Extra Font Bold Italic.ttf', ['name' => 'Extra Font', 'bold' => true, 'italic' => true]],
122+
];
123+
}
124+
125+
/**
126+
* @dataProvider providerOverride
127+
*/
128+
public function testOverrideFilenames(string $expected, array $fontArray): void
129+
{
130+
Font::setTrueTypeFontPath(self::DEFAULT_DIRECTORY);
131+
Font::setExtraFontArray([
132+
'Arial' => [
133+
'x' => 'extrafont.ttf',
134+
'xb' => 'extrafontbd.ttf',
135+
'xi' => 'extrafonti.ttf',
136+
'xbi' => 'extrafontbi.ttf',
137+
],
138+
]);
139+
$font = (new StyleFont())->applyFromArray($fontArray);
140+
$result = Font::getTrueTypeFontFileFromFont($font);
141+
self::assertSame($expected, basename($result));
142+
}
143+
144+
public function providerOverride(): array
145+
{
146+
return [
147+
['extrafont.ttf', ['name' => 'Arial']],
148+
['extrafontbd.ttf', ['name' => 'Arial', 'bold' => true]],
149+
['extrafonti.ttf', ['name' => 'Arial', 'italic' => true]],
150+
['extrafontbi.ttf', ['name' => 'Arial', 'bold' => true, 'italic' => true]],
151+
['cour.ttf', ['name' => 'Courier New']],
152+
];
153+
}
154+
}

tests/data/Shared/FakeFonts/Default/arial.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/arialbd.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/arialbi.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/ariali.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/cour.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/courbd.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/courbi.ttf

Whitespace-only changes.

tests/data/Shared/FakeFonts/Default/couri.ttf

Whitespace-only changes.

0 commit comments

Comments
 (0)