Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.41% covered (warning)
75.41%
46 / 61
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathGlyphInfoParser
75.41% covered (warning)
75.41%
46 / 61
40.00% covered (danger)
40.00%
2 / 5
29.20
0.00% covered (danger)
0.00%
0 / 1
 parse
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
7.02
 parseValueTable
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 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 MathGlyphInfo sub-table of an OpenType MATH table.
9 *
10 * The sub-table is a 4-offset header pointing to four optional
11 * inner tables. Each may be absent (offset == 0). Layout:
12 *
13 *     Offset16 mathItalicsCorrectionInfoOffset
14 *     Offset16 mathTopAccentAttachmentOffset
15 *     Offset16 extendedShapeCoverageOffset
16 *     Offset16 mathKernInfoOffset
17 *
18 * MathItalicsCorrectionInfo and MathTopAccentAttachment have
19 * identical shape:
20 *
21 *     Offset16 coverageOffset            -- Coverage table
22 *     uint16   count
23 *     MathValueRecord values[count]      -- FWord + uint16 device offset
24 *
25 * The Coverage table indexes glyphs in the values array (i-th
26 * covered glyph -> values[i]). Format 1 (list) and format 2 (ranges)
27 * both produce a glyph-ID list in coverage order.
28 *
29 * ExtendedShapeCoverage is just a Coverage table - no value array.
30 *
31 * MathKernInfo is more complex (per-corner kerning records) and
32 * lands in its own slice; this parser returns its raw bytes.
33 */
34final class MathGlyphInfoParser
35{
36    /**
37     * @var string Raw MathGlyphInfo sub-table bytes (kept on the
38     *             instance so the offset helpers can read against it).
39     */
40    private string $data = '';
41
42    public function parse(string $bytes): MathGlyphInfo
43    {
44        if (strlen($bytes) < 8) {
45            // Empty / truncated - return an "empty" struct rather
46            // than throw, so a math font with a partial table doesn't
47            // crash the whole parse.
48            return new MathGlyphInfo([], [], [], '');
49        }
50        $this->data = $bytes;
51        $italicsOffset = $this->u16(0);
52        $accentOffset = $this->u16(2);
53        $extendedOffset = $this->u16(4);
54        $kernInfoOffset = $this->u16(6);
55
56        $italicCorrections = $italicsOffset !== 0
57            ? $this->parseValueTable($italicsOffset)
58            : [];
59
60        $topAccentAttachments = $accentOffset !== 0
61            ? $this->parseValueTable($accentOffset)
62            : [];
63
64        $extendedShapes = [];
65        if ($extendedOffset !== 0) {
66            foreach ($this->parseCoverage($extendedOffset) as $gid) {
67                $extendedShapes[$gid] = true;
68            }
69        }
70
71        $kernBytes = '';
72        if ($kernInfoOffset !== 0) {
73            $kernBytes = substr($this->data, $kernInfoOffset);
74        }
75
76        return new MathGlyphInfo(
77            italicCorrections: $italicCorrections,
78            topAccentAttachments: $topAccentAttachments,
79            extendedShapes: $extendedShapes,
80            kernInfoBytes: $kernBytes,
81        );
82    }
83
84    /**
85     * Parse a coverage + value-array sub-table (the shape shared by
86     * MathItalicsCorrectionInfo and MathTopAccentAttachment).
87     *
88     * @return array<int, int> gid -> FUnit value
89     */
90    private function parseValueTable(int $base): array
91    {
92        $coverageOffset = $base + $this->u16($base);
93        $count = $this->u16($base + 2);
94        $valuesBase = $base + 4;
95        $glyphs = $this->parseCoverage($coverageOffset);
96        $out = [];
97        $bound = min($count, count($glyphs));
98        for ($i = 0; $i < $bound; $i++) {
99            // MathValueRecord = Int16 FWord + uint16 device offset.
100            // Skip the device offset; the painter doesn't hint.
101            $fword = $this->i16($valuesBase + $i * 4);
102            $out[$glyphs[$i]] = $fword;
103        }
104        return $out;
105    }
106
107    /**
108     * Parse a Coverage table at the given offset within $this->data.
109     * Both format 1 (glyph list) and format 2 (range records) yield
110     * the same flat list of glyph IDs in coverage order.
111     *
112     * @return list<int>
113     */
114    private function parseCoverage(int $offset): array
115    {
116        if ($offset < 0 || $offset + 4 > strlen($this->data)) {
117            return [];
118        }
119        $format = $this->u16($offset);
120        if ($format === 1) {
121            $count = $this->u16($offset + 2);
122            $glyphs = [];
123            for ($i = 0; $i < $count; $i++) {
124                $glyphs[] = $this->u16($offset + 4 + $i * 2);
125            }
126            return $glyphs;
127        }
128        if ($format === 2) {
129            $rangeCount = $this->u16($offset + 2);
130            $glyphs = [];
131            for ($i = 0; $i < $rangeCount; $i++) {
132                $base = $offset + 4 + $i * 6;
133                $start = $this->u16($base);
134                $end = $this->u16($base + 2);
135                // RangeRecord also carries a startCoverageIndex at
136                // base+4 but we don't need it - coverage index is
137                // implied by position in the glyphs list.
138                for ($g = $start; $g <= $end; $g++) {
139                    $glyphs[] = $g;
140                }
141            }
142            return $glyphs;
143        }
144        return [];
145    }
146
147    private function u16(int $offset): int
148    {
149        if ($offset < 0 || $offset + 1 >= strlen($this->data)) {
150            return 0;
151        }
152        return unpack('n', $this->data, $offset)[1];
153    }
154
155    private function i16(int $offset): int
156    {
157        $v = $this->u16($offset);
158        return $v >= 0x8000 ? $v - 0x10000 : $v;
159    }
160}