-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: Implement a font registry for text rendering (#191)
* feat!: Implement a font registry for text rendering Fixes #82, #168, #171. This patch removes the artificial `SVGFont` class as well as the `SVGText::setFont(SVGFont $font)` method. Instead, it is now possible to register TrueType (`.ttf`) font files by calling `SVG::addFont()` with a file path. A TrueType font file parser is implemented based on Microsoft's OpenType specification. This allows us to choose the best matching font file for a given text element automatically, just like a browser would, based on algorithms from CSS. Currently, TTF files are supported in the "Regular", "Bold", "Italic", and "Bold Italic" variants. The simple algorithm implemented here tries to match the font family exactly, if possible, falling back to any other font. Generic font families such as "serif" or "monospace" aren't supported yet and will trigger the fallback. Also, the relative weights "bolder" and "lighter" aren't computed and fall back to normal weight. BREAKING CHANGE: `SVGFont` class and some methods on `SVGText` removed. * feat: Implement support for the TrueType "OS/2" table for font weight By looking at the "OS/2" TTF table in addition to the "name" table, we can gather additional info about the font weight besides whether a font is bold or not. By doing so, we gain support for every weight level! :) * fix: Add all missing type declarations in TrueTypeFontFile
- Loading branch information
Showing
15 changed files
with
521 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace SVG\Fonts; | ||
|
||
/** | ||
* Abstract base class for font files. | ||
*/ | ||
abstract class FontFile | ||
{ | ||
private $path; | ||
|
||
public function __construct(string $path) | ||
{ | ||
$this->path = $path; | ||
} | ||
|
||
/** | ||
* @return string The path of the font file. | ||
*/ | ||
public function getPath(): string | ||
{ | ||
return $this->path; | ||
} | ||
|
||
abstract public function getFamily(): string; | ||
|
||
abstract public function getWeight(): float; | ||
|
||
abstract public function isItalic(): bool; | ||
|
||
abstract public function isMonospace(): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?php | ||
|
||
namespace SVG\Fonts; | ||
|
||
class FontRegistry | ||
{ | ||
private $fontFiles = []; | ||
|
||
public function addFont(string $filePath): void | ||
{ | ||
$ttfFile = TrueTypeFontFile::read($filePath); | ||
if ($ttfFile === null) { | ||
throw new \RuntimeException('Font file "' . $filePath . '" is not a valid TrueType font.'); | ||
} | ||
$this->fontFiles[] = $ttfFile; | ||
} | ||
|
||
public function findMatchingFont(?string $family, bool $italic, float $weight): ?FontFile | ||
{ | ||
if (empty($this->fontFiles)) { | ||
return null; | ||
} | ||
|
||
// TODO implement generic families ('serif', 'sans-serif', 'monospace', etc.) | ||
|
||
// Check whether the requested font family is available, or whether we don't have to bother checking the family | ||
// in the following loops. | ||
$anyFontFamily = true; | ||
foreach ($this->fontFiles as $font) { | ||
if ($family === $font->getFamily()) { | ||
$anyFontFamily = false; | ||
} | ||
} | ||
|
||
// Attempt to find the closest-weight match with correct family and italicness. | ||
$match = $this->closestMatchBasedOnWeight(function (FontFile $font) use ($family, $anyFontFamily, $italic) { | ||
return ($anyFontFamily || $font->getFamily() === $family) && $font->isItalic() === $italic; | ||
}, $weight); | ||
|
||
// Attempt to match just based on the font family. | ||
$match = $match ?? $this->closestMatchBasedOnWeight(function (FontFile $font) use ($family, $anyFontFamily) { | ||
return $anyFontFamily || $font->getFamily() === $family; | ||
}, $weight); | ||
|
||
// Return any font at all, if possible. | ||
return $match ?? $this->fontFiles[0]; | ||
} | ||
|
||
private function closestMatchBasedOnWeight(callable $filter, float $targetWeight): ?FontFile | ||
{ | ||
$bestMatch = null; | ||
foreach ($this->fontFiles as $font) { | ||
if (!$filter($font)) { | ||
continue; | ||
} | ||
if ($bestMatch === null) { | ||
$bestMatch = $font; | ||
continue; | ||
} | ||
if (abs($targetWeight - $font->getWeight()) < abs($targetWeight - $bestMatch->getWeight())) { | ||
$bestMatch = $font; | ||
} | ||
} | ||
return $bestMatch; | ||
} | ||
} |
Oops, something went wrong.