Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
DocumentFontBridge
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
5 / 5
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveDocumentFont
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 familyValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 weightToNumber
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 normaliseStyle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\HtmlToPdf\Svg;
6
7use Phpdftk\Css\Value\ListSeparator;
8use Phpdftk\Css\Value\StringValue;
9use Phpdftk\Css\Value\Value;
10use Phpdftk\Css\Value\ValueList;
11use Phpdftk\HtmlToPdf\Layout\FontResolver;
12use Phpdftk\Pdf\Core\Font\RegisteredFont;
13use Phpdftk\Pdf\Writer\Font as WriterFont;
14use Phpdftk\SvgToPdf\Text\DocumentFont;
15use Phpdftk\SvgToPdf\Text\DocumentFontProvider;
16
17/**
18 * Hands the HTML document's font machinery to SVG text painting.
19 *
20 * SVG 2 §11.5 defers font selection wholesale to CSS Fonts 4, so
21 * `<svg><text>` in an HTML document selects from exactly the same face
22 * set as the surrounding HTML — `@font-face` families included. Without
23 * this bridge the SVG translator falls back to its standalone
24 * standard-14 mapping and a `font-family: Ahem` run paints in
25 * Helvetica while the neighbouring `<div>` paints in Ahem.
26 *
27 * Matching runs through the layout {@see FontResolver} so weight and
28 * style selection follow the same CSS Fonts 4 §6 rules as HTML text.
29 * The resolver's *no family matched* answer is preserved rather than
30 * collapsed onto the document default font: returning null lets the
31 * translator keep its standard-14 behaviour for family stacks the
32 * document never registered, so wiring this bridge in changes only the
33 * runs that were demonstrably rendering in the wrong typeface.
34 */
35final readonly class DocumentFontBridge implements DocumentFontProvider
36{
37    /**
38     * @param array<string, RegisteredFont> $registeredFonts keyed by PostScript name
39     */
40    public function __construct(
41        private FontResolver $resolver,
42        private array $registeredFonts,
43    ) {}
44
45    public function resolveDocumentFont(array $families, ?string $weight, ?string $style): ?DocumentFont
46    {
47        if ($families === [] || $this->registeredFonts === []) {
48            return null;
49        }
50        $match = $this->resolver->resolveMatch(
51            self::familyValue($families),
52            self::weightToNumber($weight),
53            self::normaliseStyle($style),
54        );
55        if ($match === null) {
56            return null;
57        }
58        $registered = $this->registeredFonts[$match->face->data->postScriptName] ?? null;
59        if ($registered === null) {
60            return null;
61        }
62        return new DocumentFont(
63            $registered,
64            $registered instanceof WriterFont ? $registered->getUnicodeToGidMap() : [],
65        );
66    }
67
68    /**
69     * Rebuild the SVG side's plain family list into the comma-separated
70     * `font-family` {@see Value} the layout resolver consumes.
71     *
72     * @param list<string> $families
73     */
74    private static function familyValue(array $families): Value
75    {
76        return new ValueList(
77            array_map(static fn(string $name): Value => new StringValue($name), $families),
78            ListSeparator::Comma,
79        );
80    }
81
82    /**
83     * CSS Fonts 4 §2.3 — map the raw `font-weight` string onto the
84     * numeric axis the resolver matches against. `bolder` / `lighter`
85     * are relative to the parent's computed weight, which the SVG
86     * accessors don't expose; they collapse onto the values they take
87     * against the initial `normal` parent.
88     */
89    private static function weightToNumber(?string $weight): int
90    {
91        if ($weight === null) {
92            return 400;
93        }
94        $value = strtolower(trim($weight));
95        if (is_numeric($value)) {
96            return (int) max(1.0, min(1000.0, (float) $value));
97        }
98        return match ($value) {
99            'bold', 'bolder' => 700,
100            'lighter' => 100,
101            default => 400,
102        };
103    }
104
105    private static function normaliseStyle(?string $style): string
106    {
107        $value = strtolower(trim($style ?? ''));
108        return match ($value) {
109            'italic', 'oblique' => $value,
110            default => 'normal',
111        };
112    }
113}