Skip to content

Commit 6968211

Browse files
committed
added TrueType Format 14 support
1 parent 6d9c60b commit 6968211

2 files changed

Lines changed: 231 additions & 3 deletions

File tree

src/Import/TrueType.php

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,8 +1131,6 @@ protected function processFormat12(): void
11311131
* 0 - uint32 startCharCode First character code in this group
11321132
* 4 - uint32 endCharCode Last character code in this group
11331133
* 8 - uint32 glyphID Glyph index to be used for all characters in the group's range
1134-
*
1135-
* @link https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-13-many-to-one-range-mappings
11361134
*/
11371135
protected function processFormat13(): void
11381136
{
@@ -1154,8 +1152,72 @@ protected function processFormat13(): void
11541152

11551153
/**
11561154
* Process Format 14: Unicode Variation Sequences
1155+
*
1156+
* 'cmap' Subtable Format 14 (format field already consumed, $this->offset points past it):
1157+
* 0 - uint16 format (already consumed) Always 14
1158+
* 2 - uint32 length Byte length of this subtable (incl. header)
1159+
* 6 - uint32 numVarSelectorRecords Number of VariationSelector records
1160+
* 10 - VariationSelector[numVarSelectorRecords] varSelector Array of VariationSelector records
1161+
*
1162+
* VariationSelector Record (11 bytes):
1163+
* 0 - uint24 varSelector Variation selector value
1164+
* 3 - Offset32 defaultUVSOffset Offset from subtable start to Default UVS table; 0 if absent
1165+
* 7 - Offset32 nonDefaultUVSOffset Offset from subtable start to Non-Default UVS table; 0 if absent
1166+
*
1167+
* NonDefaultUVS Table:
1168+
* 0 - uint32 numUVSMappings Number of UVS Mapping records
1169+
* 4 - UVSMapping[] uvsMappings Array of UVSMapping records
1170+
*
1171+
* UVSMapping Record (5 bytes):
1172+
* 0 - uint24 unicodeValue Base Unicode code point of the variation sequence
1173+
* 3 - uint16 glyphID Glyph ID to use for this variation sequence
1174+
*
1175+
* Default UVS sequences reuse the glyph already mapped in the main cmap table; no action required.
11571176
*/
11581177
protected function processFormat14(): void
11591178
{
1179+
// The format field (uint16) was consumed before this method was called,
1180+
// so the subtable starts 2 bytes before the current offset.
1181+
$subtableOffset = ($this->offset - 2);
1182+
1183+
$this->offset += 4; // skip length (uint32)
1184+
1185+
$numVarSelectors = $this->fbyte->getULong($this->offset);
1186+
$this->offset += 4;
1187+
1188+
for ($idx = 0; $idx < $numVarSelectors; ++$idx) {
1189+
$this->offset += 3; // skip varSelector (uint24)
1190+
1191+
$this->offset += 4; // skip defaultUVSOffset — default sequences use the main cmap glyph
1192+
1193+
$nonDefaultOffset = $this->fbyte->getULong($this->offset);
1194+
$this->offset += 4;
1195+
1196+
if ($nonDefaultOffset === 0) {
1197+
continue;
1198+
}
1199+
1200+
// Process the Non-Default UVS table for this variation selector.
1201+
$savedOffset = $this->offset;
1202+
$this->offset = ($subtableOffset + $nonDefaultOffset);
1203+
1204+
$numUVSMappings = $this->fbyte->getULong($this->offset);
1205+
$this->offset += 4;
1206+
1207+
for ($jdx = 0; $jdx < $numUVSMappings; ++$jdx) {
1208+
// unicodeValue: uint24 (3 bytes, big-endian)
1209+
$unicodeValue = ($this->fbyte->getByte($this->offset) << 16)
1210+
| ($this->fbyte->getByte($this->offset + 1) << 8)
1211+
| $this->fbyte->getByte($this->offset + 2);
1212+
$this->offset += 3;
1213+
1214+
$glyphID = $this->fbyte->getUShort($this->offset);
1215+
$this->offset += 2;
1216+
1217+
$this->addCtgItem($unicodeValue, $glyphID);
1218+
}
1219+
1220+
$this->offset = $savedOffset;
1221+
}
11601222
}
11611223
}

test/TrueTypeTest.php

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,175 @@ public function testProcessFormat13AddsGlyphsToSubset(): void
157157
}
158158

159159

160+
public function testProcessFormat14NonDefaultUVSMapsGlyphs(): void
161+
{
162+
// Format 14 subtable with 1 VariationSelector record that has a Non-Default UVS table.
163+
//
164+
// Byte layout (subtable starts at offset 0):
165+
// 0- 1: format = 14 (0x00,0x0E) — consumed by getCIDToGIDMap before this call
166+
// 2- 5: length = 30 (0x00,0x00,0x00,0x1E)
167+
// 6- 9: numVarSelectorRecs = 1 (0x00,0x00,0x00,0x01)
168+
// 10-12: varSelector = 0x0E0100 (3 bytes)
169+
// 13-16: defaultUVSOffset = 0 (absent)
170+
// 17-20: nonDefaultUVSOffset = 21 (0x00,0x00,0x00,0x15) — relative to subtable start
171+
// 21-24: numUVSMappings = 1 (0x00,0x00,0x00,0x01)
172+
// 25-27: unicodeValue = U+0082A6 (0x00,0x82,0xA6)
173+
// 28-29: glyphID = 1142 (0x04,0x76)
174+
$font = "\x00\x0e" // format = 14
175+
. "\x00\x00\x00\x1e" // length = 30
176+
. "\x00\x00\x00\x01" // numVarSelectorRecords = 1
177+
. "\x0e\x01\x00" // varSelector = U+E0100 (uint24)
178+
. "\x00\x00\x00\x00" // defaultUVSOffset = 0 (absent)
179+
. "\x00\x00\x00\x15" // nonDefaultUVSOffset = 21
180+
. "\x00\x00\x00\x01" // numUVSMappings = 1
181+
. "\x00\x82\xa6" // unicodeValue = U+0082A6 (uint24)
182+
. "\x04\x76"; // glyphID = 1142
183+
184+
$instance = $this->buildTrueType($font, [
185+
'encodingTables' => [
186+
[
187+
'platformID' => 3,
188+
'encodingID' => 1,
189+
'offset' => 0,
190+
],
191+
],
192+
'platform_id' => 3,
193+
'encoding_id' => 1,
194+
'table' => [
195+
'cmap' => [
196+
'offset' => 0,
197+
],
198+
],
199+
'type' => 'TrueTypeUnicode',
200+
]);
201+
202+
$this->invokeMethod($instance, 'getCIDToGIDMap');
203+
$fontData = $this->getFontData($instance);
204+
205+
// Non-default UVS mapping: U+0082A6 → glyph 1142
206+
$this->assertSame(1142, $fontData['ctgdata'][0x0082A6]);
207+
// .notdef fallback must still be set
208+
$this->assertSame(0, $fontData['ctgdata'][0]);
209+
}
210+
211+
public function testProcessFormat14NonDefaultUVSTracksSubglyphs(): void
212+
{
213+
// Same layout as above but with a second mapping and subchars tracking.
214+
//
215+
// Byte layout (subtable starts at offset 0):
216+
// 0- 1: format = 14 (0x00,0x0E)
217+
// 2- 5: length = 35
218+
// 6- 9: numVarSelectorRecs = 1
219+
// 10-12: varSelector = 0x0E0101 (3 bytes)
220+
// 13-16: defaultUVSOffset = 0
221+
// 17-20: nonDefaultUVSOffset = 21
222+
// 21-24: numUVSMappings = 2
223+
// 25-27: unicodeValue[0] = U+0082A6 → glyphID 7961 (0x00,0x82,0xA6 + 0x1F,0x19)
224+
// 30-32: unicodeValue[1] = U+004E4D → glyphID 42 (0x00,0x4E,0x4D + 0x00,0x2A)
225+
$font = "\x00\x0e" // format = 14
226+
. "\x00\x00\x00\x25" // length = 37
227+
. "\x00\x00\x00\x01" // numVarSelectorRecords = 1
228+
. "\x0e\x01\x01" // varSelector = U+E0101 (uint24)
229+
. "\x00\x00\x00\x00" // defaultUVSOffset = 0
230+
. "\x00\x00\x00\x15" // nonDefaultUVSOffset = 21
231+
. "\x00\x00\x00\x02" // numUVSMappings = 2
232+
. "\x00\x82\xa6" // unicodeValue[0] = U+0082A6
233+
. "\x1f\x19" // glyphID[0] = 7961
234+
. "\x00\x4e\x4d" // unicodeValue[1] = U+004E4D
235+
. "\x00\x2a"; // glyphID[1] = 42
236+
237+
$instance = $this->buildTrueType($font, [
238+
'encodingTables' => [
239+
[
240+
'platformID' => 3,
241+
'encodingID' => 1,
242+
'offset' => 0,
243+
],
244+
],
245+
'platform_id' => 3,
246+
'encoding_id' => 1,
247+
'table' => [
248+
'cmap' => [
249+
'offset' => 0,
250+
],
251+
],
252+
'type' => 'TrueTypeUnicode',
253+
]);
254+
255+
// Mark U+0082A6 as a subset char to verify subglyphs tracking
256+
$this->setProperty($instance, 'subchars', [0x0082A6 => true]);
257+
258+
$this->invokeMethod($instance, 'getCIDToGIDMap');
259+
$fontData = $this->getFontData($instance);
260+
$subGlyphs = $this->getProperty($instance, 'subglyphs');
261+
262+
$this->assertSame(7961, $fontData['ctgdata'][0x0082A6]);
263+
$this->assertSame(42, $fontData['ctgdata'][0x004E4D]);
264+
// Glyph 7961 must be in the subset (0x0082A6 was a subchar)
265+
$this->assertArrayHasKey(7961, $subGlyphs);
266+
$this->assertTrue($subGlyphs[7961]);
267+
// Glyph 42 must NOT be in the subset (U+004E4D was not a subchar)
268+
$this->assertArrayNotHasKey(42, $subGlyphs);
269+
}
270+
271+
public function testProcessFormat14DefaultUVSOnlyAddsNoCtgEntries(): void
272+
{
273+
// Format 14 subtable with 1 VariationSelector record that has only a Default UVS table.
274+
// Default UVS sequences use the standard cmap glyph — no ctgdata entries should be added.
275+
//
276+
// Byte layout (subtable starts at offset 0):
277+
// 0- 1: format = 14
278+
// 2- 5: length = 26
279+
// 6- 9: numVarSelectorRecs = 1
280+
// 10-12: varSelector = 0x0E0100
281+
// 13-16: defaultUVSOffset = 21 (has a Default UVS table)
282+
// 17-20: nonDefaultUVSOffset = 0 (absent)
283+
// 21-24: numUnicodeValueRanges = 1
284+
// 25-27: startUnicodeValue = U+004E4D (uint24)
285+
// 28: additionalCount = 2
286+
$font = "\x00\x0e" // format = 14
287+
. "\x00\x00\x00\x1d" // length = 29
288+
. "\x00\x00\x00\x01" // numVarSelectorRecords = 1
289+
. "\x0e\x01\x00" // varSelector = U+E0100 (uint24)
290+
. "\x00\x00\x00\x15" // defaultUVSOffset = 21
291+
. "\x00\x00\x00\x00" // nonDefaultUVSOffset = 0 (absent)
292+
. "\x00\x00\x00\x01" // numUnicodeValueRanges = 1
293+
. "\x00\x4e\x4d" // startUnicodeValue = U+004E4D (uint24)
294+
. "\x02"; // additionalCount = 2
295+
296+
$instance = $this->buildTrueType($font, [
297+
'encodingTables' => [
298+
[
299+
'platformID' => 3,
300+
'encodingID' => 1,
301+
'offset' => 0,
302+
],
303+
],
304+
'platform_id' => 3,
305+
'encoding_id' => 1,
306+
'table' => [
307+
'cmap' => [
308+
'offset' => 0,
309+
],
310+
],
311+
'type' => 'TrueTypeUnicode',
312+
]);
313+
314+
$this->invokeMethod($instance, 'getCIDToGIDMap');
315+
$fontData = $this->getFontData($instance);
316+
317+
// Default UVS adds no explicit ctgdata entries beyond .notdef
318+
$this->assertSame([0 => 0], $fontData['ctgdata']);
319+
}
320+
160321
public function testGetCIDToGIDMapFormat14SetsNotDefGlyph(): void
161322
{
162-
$instance = $this->buildTrueType("\x00\x0e", [
323+
// Format 14 subtable with numVarSelectorRecords=0: no mappings → only .notdef fallback added.
324+
$font = "\x00\x0e" // format = 14
325+
. "\x00\x00\x00\x0a" // length = 10
326+
. "\x00\x00\x00\x00"; // numVarSelectorRecords = 0
327+
328+
$instance = $this->buildTrueType($font, [
163329
'encodingTables' => [
164330
[
165331
'platformID' => 3,

0 commit comments

Comments
 (0)