Skip to content

Commit

Permalink
feat!: Implement a font registry for text rendering (#191)
Browse files Browse the repository at this point in the history
* 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
meyfa authored Jan 2, 2023
1 parent ef0ca16 commit 24beb69
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 128 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,40 @@ $rasterImage = $image->toRasterImage(200, 200, '#FFFFFF');
imagejpeg($rasterImage, 'path/to/output.jpg');
```

### Text rendering (loading fonts)

PHP-SVG implements support for TrueType fonts (`.ttf` files) using a handcrafted TTF parser. Since PHP doesn't come
with any built-in font files, you will need to provide your own. The following example shows how to load a set of font
files. PHP-SVG will try to pick the best matching font for a given text element, based on algorithms from the CSS spec.

```php
<?php
require __DIR__ . '/vendor/autoload.php';

use SVG\SVG;

// load a set of fonts
SVG::addFont(FONTS_DIR . 'Ubuntu-Regular.ttf');
SVG::addFont(FONTS_DIR . 'Ubuntu-Bold.ttf');
SVG::addFont(FONTS_DIR . 'Ubuntu-Italic.ttf');
SVG::addFont(FONTS_DIR . 'Ubuntu-BoldItalic.ttf');

$image = SVG::fromString('
<svg width="220" height="220">
<rect x="0" y="0" width="100%" height="100%" fill="lightgray"/>
<g font-size="15">
<text y="20">hello world</text>
<text y="100" font-weight="bold">in bold!</text>
<text y="120" font-style="italic">and italic!</text>
<text y="140" font-weight="bold" font-style="italic">bold and italic</text>
</g>
</svg>
');

header('Content-Type: image/png');
imagepng($image->toRasterImage(220, 220));
```


## Document model

Expand Down
32 changes: 32 additions & 0 deletions src/Fonts/FontFile.php
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;
}
66 changes: 66 additions & 0 deletions src/Fonts/FontRegistry.php
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;
}
}
Loading

0 comments on commit 24beb69

Please sign in to comment.