Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
80 / 85
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathVariantsParser
94.12% covered (success)
94.12%
80 / 85
50.00% covered (danger)
50.00%
3 / 6
26.14
0.00% covered (danger)
0.00%
0 / 1
 parse
94.29% covered (success)
94.29%
33 / 35
0.00% covered (danger)
0.00%
0 / 1
8.01
 parseConstruction
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 parseAssembly
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 parseCoverage
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 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 MathVariants sub-table of an OpenType MATH table.
9 *
10 * MathVariants layout:
11 *
12 *     uint16   minConnectorOverlap
13 *     Offset16 vertGlyphCoverageOffset
14 *     Offset16 horizGlyphCoverageOffset
15 *     uint16   vertGlyphCount
16 *     uint16   horizGlyphCount
17 *     Offset16 vertGlyphConstructionOffsets[vertGlyphCount]
18 *     Offset16 horizGlyphConstructionOffsets[horizGlyphCount]
19 *
20 * Each MathGlyphConstruction:
21 *
22 *     Offset16 glyphAssemblyOffset (0 if absent)
23 *     uint16   variantCount
24 *     MathGlyphVariantRecord variants[variantCount]:
25 *         uint16 variantGlyph
26 *         uint16 advanceMeasurement
27 *
28 * Each MathGlyphAssembly:
29 *
30 *     MathValueRecord italicsCorrection
31 *     uint16 partCount
32 *     GlyphPartRecord parts[partCount]:
33 *         uint16 glyphID
34 *         uint16 startConnectorLength
35 *         uint16 endConnectorLength
36 *         uint16 fullAdvance
37 *         uint16 partFlags (bit 0 = extender)
38 *
39 * Coverage tables follow standard OpenType format 1 or 2.
40 */
41final class MathVariantsParser
42{
43    private string $data = '';
44
45    public function parse(string $bytes): MathVariants
46    {
47        if (strlen($bytes) < 10) {
48            return new MathVariants(0, [], []);
49        }
50        $this->data = $bytes;
51        $minConnectorOverlap = $this->u16(0);
52        $vertCoverageOffset = $this->u16(2);
53        $horizCoverageOffset = $this->u16(4);
54        $vertCount = $this->u16(6);
55        $horizCount = $this->u16(8);
56
57        $vertGlyphs = $vertCoverageOffset !== 0
58            ? $this->parseCoverage($vertCoverageOffset)
59            : [];
60        $horizGlyphs = $horizCoverageOffset !== 0
61            ? $this->parseCoverage($horizCoverageOffset)
62            : [];
63
64        $vertOffsetsBase = 10;
65        $horizOffsetsBase = 10 + $vertCount * 2;
66
67        $vertConstructions = [];
68        $vertBound = min($vertCount, count($vertGlyphs));
69        for ($i = 0; $i < $vertBound; $i++) {
70            $offset = $this->u16($vertOffsetsBase + $i * 2);
71            if ($offset === 0) {
72                continue;
73            }
74            $vertConstructions[$vertGlyphs[$i]] = $this->parseConstruction($offset);
75        }
76
77        $horizConstructions = [];
78        $horizBound = min($horizCount, count($horizGlyphs));
79        for ($i = 0; $i < $horizBound; $i++) {
80            $offset = $this->u16($horizOffsetsBase + $i * 2);
81            if ($offset === 0) {
82                continue;
83            }
84            $horizConstructions[$horizGlyphs[$i]] = $this->parseConstruction($offset);
85        }
86
87        return new MathVariants(
88            minConnectorOverlap: $minConnectorOverlap,
89            verticalConstructions: $vertConstructions,
90            horizontalConstructions: $horizConstructions,
91        );
92    }
93
94    private function parseConstruction(int $base): MathGlyphConstruction
95    {
96        $assemblyOffset = $this->u16($base);
97        $variantCount = $this->u16($base + 2);
98        $variants = [];
99        for ($i = 0; $i < $variantCount; $i++) {
100            $variants[] = [
101                'glyphId' => $this->u16($base + 4 + $i * 4),
102                'advance' => $this->u16($base + 4 + $i * 4 + 2),
103            ];
104        }
105
106        $assembly = null;
107        if ($assemblyOffset !== 0) {
108            // The assembly offset is relative to the construction base.
109            $assembly = $this->parseAssembly($base + $assemblyOffset);
110        }
111
112        return new MathGlyphConstruction($variants, $assembly);
113    }
114
115    private function parseAssembly(int $base): MathGlyphAssembly
116    {
117        // MathValueRecord italicsCorrection: Int16 FWord + uint16 device.
118        $italics = $this->i16($base);
119        $partCount = $this->u16($base + 4);
120        $parts = [];
121        for ($i = 0; $i < $partCount; $i++) {
122            $rb = $base + 6 + $i * 10;
123            $parts[] = [
124                'glyphId' => $this->u16($rb),
125                'startConnector' => $this->u16($rb + 2),
126                'endConnector' => $this->u16($rb + 4),
127                'fullAdvance' => $this->u16($rb + 6),
128                'extender' => ($this->u16($rb + 8) & 0x0001) !== 0,
129            ];
130        }
131        return new MathGlyphAssembly(italicsCorrection: $italics, parts: $parts);
132    }
133
134    /**
135     * @return list<int>
136     */
137    private function parseCoverage(int $offset): array
138    {
139        if ($offset < 0 || $offset + 4 > strlen($this->data)) {
140            return [];
141        }
142        $format = $this->u16($offset);
143        if ($format === 1) {
144            $count = $this->u16($offset + 2);
145            $glyphs = [];
146            for ($i = 0; $i < $count; $i++) {
147                $glyphs[] = $this->u16($offset + 4 + $i * 2);
148            }
149            return $glyphs;
150        }
151        if ($format === 2) {
152            $rangeCount = $this->u16($offset + 2);
153            $glyphs = [];
154            for ($i = 0; $i < $rangeCount; $i++) {
155                $rb = $offset + 4 + $i * 6;
156                $start = $this->u16($rb);
157                $end = $this->u16($rb + 2);
158                for ($g = $start; $g <= $end; $g++) {
159                    $glyphs[] = $g;
160                }
161            }
162            return $glyphs;
163        }
164        return [];
165    }
166
167    private function u16(int $offset): int
168    {
169        if ($offset < 0 || $offset + 1 >= strlen($this->data)) {
170            return 0;
171        }
172        return unpack('n', $this->data, $offset)[1];
173    }
174
175    private function i16(int $offset): int
176    {
177        $v = $this->u16($offset);
178        return $v >= 0x8000 ? $v - 0x10000 : $v;
179    }
180}