Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.67% covered (warning)
76.67%
46 / 60
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathKernInfoParser
76.67% covered (warning)
76.67%
46 / 60
40.00% covered (danger)
40.00%
2 / 5
32.94
0.00% covered (danger)
0.00%
0 / 1
 parse
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
9
 parseKern
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 parseCoverage
40.00% covered (danger)
40.00%
8 / 20
0.00% covered (danger)
0.00%
0 / 1
21.82
 u16
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 i16
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\FontParser;
6
7/**
8 * Parser for the MathKernInfo sub-table of an OpenType MATH table.
9 *
10 * Layout:
11 *
12 *     Offset16 mathKernCoverageOffset
13 *     uint16   mathKernCount
14 *     MathKernInfoRecord records[count]:
15 *         Offset16 topRightMathKernOffset
16 *         Offset16 topLeftMathKernOffset
17 *         Offset16 bottomRightMathKernOffset
18 *         Offset16 bottomLeftMathKernOffset
19 *
20 * Each MathKern (per-corner) layout:
21 *
22 *     uint16 heightCount
23 *     MathValueRecord correctionHeights[heightCount]
24 *     MathValueRecord kernValues[heightCount + 1]
25 *
26 * All offsets are relative to the start of MathKernInfo (the
27 * sub-table this parser receives as bytes).
28 *
29 * Empty / truncated input returns an empty {@see MathKernInfo}
30 * rather than throwing - fonts in the wild ship partial tables.
31 */
32final class MathKernInfoParser
33{
34    private string $data = '';
35
36    public function parse(string $bytes): MathKernInfo
37    {
38        if (strlen($bytes) < 4) {
39            return new MathKernInfo([]);
40        }
41        $this->data = $bytes;
42        $coverageOffset = $this->u16(0);
43        $count = $this->u16(2);
44        if ($coverageOffset === 0 || $count === 0) {
45            return new MathKernInfo([]);
46        }
47        $glyphs = $this->parseCoverage($coverageOffset);
48        $records = [];
49        $recordsBase = 4;
50        $bound = min($count, count($glyphs));
51        for ($i = 0; $i < $bound; $i++) {
52            $recBase = $recordsBase + $i * 8;
53            $tr = $this->u16($recBase);
54            $tl = $this->u16($recBase + 2);
55            $br = $this->u16($recBase + 4);
56            $bl = $this->u16($recBase + 6);
57            $records[$glyphs[$i]] = new MathKernRecord(
58                topRight: $tr !== 0 ? $this->parseKern($tr) : null,
59                topLeft: $tl !== 0 ? $this->parseKern($tl) : null,
60                bottomRight: $br !== 0 ? $this->parseKern($br) : null,
61                bottomLeft: $bl !== 0 ? $this->parseKern($bl) : null,
62            );
63        }
64        return new MathKernInfo($records);
65    }
66
67    private function parseKern(int $offset): MathKern
68    {
69        $heightCount = $this->u16($offset);
70        $heights = [];
71        $heightBase = $offset + 2;
72        for ($i = 0; $i < $heightCount; $i++) {
73            // MathValueRecord = Int16 FWord + uint16 device offset.
74            $heights[] = $this->i16($heightBase + $i * 4);
75        }
76        $valuesBase = $heightBase + $heightCount * 4;
77        $kerns = [];
78        $kernCount = $heightCount + 1;
79        for ($i = 0; $i < $kernCount; $i++) {
80            $kerns[] = $this->i16($valuesBase + $i * 4);
81        }
82        return new MathKern(correctionHeights: $heights, kernValues: $kerns);
83    }
84
85    /**
86     * @return list<int>
87     */
88    private function parseCoverage(int $offset): array
89    {
90        if ($offset < 0 || $offset + 4 > strlen($this->data)) {
91            return [];
92        }
93        $format = $this->u16($offset);
94        if ($format === 1) {
95            $count = $this->u16($offset + 2);
96            $glyphs = [];
97            for ($i = 0; $i < $count; $i++) {
98                $glyphs[] = $this->u16($offset + 4 + $i * 2);
99            }
100            return $glyphs;
101        }
102        if ($format === 2) {
103            $rangeCount = $this->u16($offset + 2);
104            $glyphs = [];
105            for ($i = 0; $i < $rangeCount; $i++) {
106                $rb = $offset + 4 + $i * 6;
107                $start = $this->u16($rb);
108                $end = $this->u16($rb + 2);
109                for ($g = $start; $g <= $end; $g++) {
110                    $glyphs[] = $g;
111                }
112            }
113            return $glyphs;
114        }
115        return [];
116    }
117
118    private function u16(int $offset): int
119    {
120        if ($offset < 0 || $offset + 1 >= strlen($this->data)) {
121            return 0;
122        }
123        return unpack('n', $this->data, $offset)[1];
124    }
125
126    private function i16(int $offset): int
127    {
128        $v = $this->u16($offset);
129        return $v >= 0x8000 ? $v - 0x10000 : $v;
130    }
131}