blob: 44ef4a055f5a755a68c67c673cff6f45e12fea7b [file] [log] [blame]
bashi@chromium.orga5748662011-03-18 17:48:29 +00001// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "gsub.h"
6
7#include <limits>
8#include <vector>
9
10#include "gdef.h"
11#include "gpos.h"
12#include "layout.h"
13#include "maxp.h"
14
15// GSUB - The Glyph Substitution Table
16// http://www.microsoft.com/typography/otspec/gsub.htm
17
18namespace {
19
20// The GSUB header size
21const size_t kGsubHeaderSize = 8;
22
23enum GSUB_TYPE {
bashi@chromium.orgc486cfb2011-03-23 06:08:30 +000024 GSUB_TYPE_SINGLE = 1,
25 GSUB_TYPE_MULTIPLE = 2,
26 GSUB_TYPE_ALTERNATE = 3,
27 GSUB_TYPE_LIGATURE = 4,
28 GSUB_TYPE_CONTEXT = 5,
29 GSUB_TYPE_CHANGING_CONTEXT = 6,
30 GSUB_TYPE_EXTENSION_SUBSTITUTION = 7,
31 GSUB_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE = 8,
bashi@chromium.orga5748662011-03-18 17:48:29 +000032 GSUB_TYPE_RESERVED = 9
33};
34
35// Lookup type parsers.
36bool ParseSingleSubstitution(const ots::OpenTypeFile *file,
37 const uint8_t *data, const size_t length);
38bool ParseMutipleSubstitution(const ots::OpenTypeFile *file,
39 const uint8_t *data, const size_t length);
40bool ParseAlternateSubstitution(const ots::OpenTypeFile *file,
41 const uint8_t *data, const size_t length);
42bool ParseLigatureSubstitution(const ots::OpenTypeFile *file,
43 const uint8_t *data, const size_t length);
44bool ParseContextSubstitution(const ots::OpenTypeFile *file,
45 const uint8_t *data, const size_t length);
46bool ParseChainingContextSubstitution(const ots::OpenTypeFile *file,
47 const uint8_t *data,
48 const size_t length);
49bool ParseExtensionSubstitution(const ots::OpenTypeFile *file,
50 const uint8_t *data, const size_t length);
51bool ParseReverseChainingContextSingleSubstitution(
52 const ots::OpenTypeFile *file, const uint8_t *data, const size_t length);
53
54const ots::LookupSubtableParser::TypeParser kGsubTypeParsers[] = {
bashi@chromium.orgc486cfb2011-03-23 06:08:30 +000055 {GSUB_TYPE_SINGLE, ParseSingleSubstitution},
56 {GSUB_TYPE_MULTIPLE, ParseMutipleSubstitution},
57 {GSUB_TYPE_ALTERNATE, ParseAlternateSubstitution},
58 {GSUB_TYPE_LIGATURE, ParseLigatureSubstitution},
59 {GSUB_TYPE_CONTEXT, ParseContextSubstitution},
60 {GSUB_TYPE_CHANGING_CONTEXT, ParseChainingContextSubstitution},
61 {GSUB_TYPE_EXTENSION_SUBSTITUTION, ParseExtensionSubstitution},
62 {GSUB_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE,
bashi@chromium.orga5748662011-03-18 17:48:29 +000063 ParseReverseChainingContextSingleSubstitution}
64};
65
66const ots::LookupSubtableParser kGsubLookupSubtableParser = {
bashi@chromium.orgc486cfb2011-03-23 06:08:30 +000067 GSUB_TYPE_RESERVED, GSUB_TYPE_EXTENSION_SUBSTITUTION, kGsubTypeParsers
bashi@chromium.orga5748662011-03-18 17:48:29 +000068};
69
70// Lookup Type 1:
71// Single Substitution Subtable
72bool ParseSingleSubstitution(const ots::OpenTypeFile *file,
73 const uint8_t *data, const size_t length) {
74 ots::Buffer subtable(data, length);
75
76 uint16_t format = 0;
77 uint16_t offset_coverage = 0;
78
79 if (!subtable.ReadU16(&format) ||
80 !subtable.ReadU16(&offset_coverage)) {
81 return OTS_FAILURE();
82 }
83
84 const uint16_t num_glyphs = file->maxp->num_glyphs;
85 if (format == 1) {
86 // Parse SingleSubstFormat1
87 int16_t delta_glyph_id = 0;
88 if (!subtable.ReadS16(&delta_glyph_id)) {
89 return OTS_FAILURE();
90 }
91 if (std::abs(delta_glyph_id) >= num_glyphs) {
92 return OTS_FAILURE();
93 }
94 } else if (format == 2) {
95 // Parse SingleSubstFormat2
96 uint16_t glyph_count = 0;
97 if (!subtable.ReadU16(&glyph_count)) {
98 return OTS_FAILURE();
99 }
100 if (glyph_count > num_glyphs) {
101 return OTS_FAILURE();
102 }
103 for (unsigned i = 0; i < glyph_count; ++i) {
104 uint16_t substitute = 0;
105 if (!subtable.ReadU16(&substitute)) {
106 return OTS_FAILURE();
107 }
108 if (substitute >= num_glyphs) {
109 OTS_WARNING("too large substitute: %u", substitute);
110 return OTS_FAILURE();
111 }
112 }
113 } else {
114 return OTS_FAILURE();
115 }
116
117 if (offset_coverage < subtable.offset() || offset_coverage >= length) {
118 return OTS_FAILURE();
119 }
120 if (!ots::ParseCoverageTable(data + offset_coverage,
121 length - offset_coverage, num_glyphs)) {
122 return OTS_FAILURE();
123 }
124
125 return true;
126}
127
128bool ParseSequenceTable(const uint8_t *data, const size_t length,
129 const uint16_t num_glyphs) {
130 ots::Buffer subtable(data, length);
131
132 uint16_t glyph_count = 0;
133 if (!subtable.ReadU16(&glyph_count)) {
134 return OTS_FAILURE();
135 }
136 if (glyph_count > num_glyphs) {
137 return OTS_FAILURE();
138 }
139 for (unsigned i = 0; i < glyph_count; ++i) {
140 uint16_t substitute = 0;
141 if (!subtable.ReadU16(&substitute)) {
142 return OTS_FAILURE();
143 }
144 if (substitute >= num_glyphs) {
145 return OTS_FAILURE();
146 }
147 }
148
149 return true;
150}
151
152// Lookup Type 2:
153// Multiple Substitution Subtable
154bool ParseMutipleSubstitution(const ots::OpenTypeFile *file,
155 const uint8_t *data, const size_t length) {
156 ots::Buffer subtable(data, length);
157
158 uint16_t format = 0;
159 uint16_t offset_coverage = 0;
160 uint16_t sequence_count = 0;
161
162 if (!subtable.ReadU16(&format) ||
163 !subtable.ReadU16(&offset_coverage) ||
164 !subtable.ReadU16(&sequence_count)) {
165 return OTS_FAILURE();
166 }
167
168 if (format != 1) {
169 return OTS_FAILURE();
170 }
171
172 const uint16_t num_glyphs = file->maxp->num_glyphs;
173 const unsigned sequence_end = static_cast<unsigned>(6) +
174 sequence_count * 2;
175 if (sequence_end > std::numeric_limits<uint16_t>::max()) {
176 return OTS_FAILURE();
177 }
178 for (unsigned i = 0; i < sequence_count; ++i) {
179 uint16_t offset_sequence = 0;
180 if (!subtable.ReadU16(&offset_sequence)) {
181 return OTS_FAILURE();
182 }
183 if (offset_sequence < sequence_end || offset_sequence >= length) {
184 return OTS_FAILURE();
185 }
186 if (!ParseSequenceTable(data + offset_sequence, length - offset_sequence,
187 num_glyphs)) {
188 return OTS_FAILURE();
189 }
190 }
191
192 if (offset_coverage < sequence_end || offset_coverage >= length) {
193 return OTS_FAILURE();
194 }
195 if (!ots::ParseCoverageTable(data + offset_coverage,
196 length - offset_coverage, num_glyphs)) {
197 return OTS_FAILURE();
198 }
199
200 return true;
201}
202
203bool ParseAlternateSetTable(const uint8_t *data, const size_t length,
204 const uint16_t num_glyphs) {
205 ots::Buffer subtable(data, length);
206
207 uint16_t glyph_count = 0;
208 if (!subtable.ReadU16(&glyph_count)) {
209 return OTS_FAILURE();
210 }
211 if (glyph_count > num_glyphs) {
212 return OTS_FAILURE();
213 }
214 for (unsigned i = 0; i < glyph_count; ++i) {
215 uint16_t alternate = 0;
216 if (!subtable.ReadU16(&alternate)) {
217 return OTS_FAILURE();
218 }
219 if (alternate >= num_glyphs) {
220 OTS_WARNING("too arge alternate: %u", alternate);
221 return OTS_FAILURE();
222 }
223 }
224 return true;
225}
226
227// Lookup Type 3:
228// Alternate Substitution Subtable
229bool ParseAlternateSubstitution(const ots::OpenTypeFile *file,
230 const uint8_t *data, const size_t length) {
231 ots::Buffer subtable(data, length);
232
233 uint16_t format = 0;
234 uint16_t offset_coverage = 0;
235 uint16_t alternate_set_count = 0;
236
237 if (!subtable.ReadU16(&format) ||
238 !subtable.ReadU16(&offset_coverage) ||
239 !subtable.ReadU16(&alternate_set_count)) {
240 return OTS_FAILURE();
241 }
242
243 if (format != 1) {
244 return OTS_FAILURE();
245 }
246
247 const uint16_t num_glyphs = file->maxp->num_glyphs;
248 const unsigned alternate_set_end = static_cast<unsigned>(6) +
249 alternate_set_count * 2;
250 if (alternate_set_end > std::numeric_limits<uint16_t>::max()) {
251 return OTS_FAILURE();
252 }
253 for (unsigned i = 0; i < alternate_set_count; ++i) {
254 uint16_t offset_alternate_set = 0;
255 if (!subtable.ReadU16(&offset_alternate_set)) {
256 return OTS_FAILURE();
257 }
258 if (offset_alternate_set < alternate_set_end ||
259 offset_alternate_set >= length) {
260 return OTS_FAILURE();
261 }
262 if (!ParseAlternateSetTable(data + offset_alternate_set,
263 length - offset_alternate_set,
264 num_glyphs)) {
265 return OTS_FAILURE();
266 }
267 }
268
269 if (offset_coverage < alternate_set_end || offset_coverage >= length) {
270 return OTS_FAILURE();
271 }
272 if (!ots::ParseCoverageTable(data + offset_coverage,
273 length - offset_coverage, num_glyphs)) {
274 return OTS_FAILURE();
275 }
276
277 return true;
278}
279
280bool ParseLigatureTable(const uint8_t *data, const size_t length,
281 const uint16_t num_glyphs) {
282 ots::Buffer subtable(data, length);
283
284 uint16_t lig_glyph = 0;
285 uint16_t comp_count = 0;
286
287 if (!subtable.ReadU16(&lig_glyph) ||
288 !subtable.ReadU16(&comp_count)) {
289 return OTS_FAILURE();
290 }
291
292 if (lig_glyph >= num_glyphs) {
293 OTS_WARNING("too large lig_glyph: %u", lig_glyph);
294 return OTS_FAILURE();
295 }
296 if (comp_count == 0 || comp_count > num_glyphs) {
297 return OTS_FAILURE();
298 }
299 for (unsigned i = 0; i < comp_count - static_cast<unsigned>(1); ++i) {
300 uint16_t component = 0;
301 if (!subtable.ReadU16(&component)) {
302 return OTS_FAILURE();
303 }
304 if (component >= num_glyphs) {
305 return OTS_FAILURE();
306 }
307 }
308
309 return true;
310}
311
312bool ParseLigatureSetTable(const uint8_t *data, const size_t length,
313 const uint16_t num_glyphs) {
314 ots::Buffer subtable(data, length);
315
316 uint16_t ligature_count = 0;
317
318 if (!subtable.ReadU16(&ligature_count)) {
319 return OTS_FAILURE();
320 }
321
322 const unsigned ligature_end = static_cast<unsigned>(2) + ligature_count * 2;
323 if (ligature_end > std::numeric_limits<uint16_t>::max()) {
324 return OTS_FAILURE();
325 }
326 for (unsigned i = 0; i < ligature_count; ++i) {
327 uint16_t offset_ligature = 0;
328 if (!subtable.ReadU16(&offset_ligature)) {
329 return OTS_FAILURE();
330 }
331 if (offset_ligature < ligature_end || offset_ligature >= length) {
332 return OTS_FAILURE();
333 }
334 if (!ParseLigatureTable(data + offset_ligature, length - offset_ligature,
335 num_glyphs)) {
336 return OTS_FAILURE();
337 }
338 }
339
340 return true;
341}
342
343// Lookup Type 4:
344// Ligature Substitution Subtable
345bool ParseLigatureSubstitution(const ots::OpenTypeFile *file,
346 const uint8_t *data, const size_t length) {
347 ots::Buffer subtable(data, length);
348
349 uint16_t format = 0;
350 uint16_t offset_coverage = 0;
351 uint16_t lig_set_count = 0;
352
353 if (!subtable.ReadU16(&format) ||
354 !subtable.ReadU16(&offset_coverage) ||
355 !subtable.ReadU16(&lig_set_count)) {
356 return OTS_FAILURE();
357 }
358
359 if (format != 1) {
360 return OTS_FAILURE();
361 }
362
363 const uint16_t num_glyphs = file->maxp->num_glyphs;
364 const unsigned ligature_set_end = static_cast<unsigned>(6) +
365 lig_set_count * 2;
366 if (ligature_set_end > std::numeric_limits<uint16_t>::max()) {
367 return OTS_FAILURE();
368 }
369 for (unsigned i = 0; i < lig_set_count; ++i) {
370 uint16_t offset_ligature_set = 0;
371 if (!subtable.ReadU16(&offset_ligature_set)) {
372 return OTS_FAILURE();
373 }
374 if (offset_ligature_set < ligature_set_end ||
375 offset_ligature_set >= length) {
376 return OTS_FAILURE();
377 }
378 if (!ParseLigatureSetTable(data + offset_ligature_set,
379 length - offset_ligature_set, num_glyphs)) {
380 return OTS_FAILURE();
381 }
382 }
383
384 if (offset_coverage < ligature_set_end || offset_coverage >= length) {
385 return OTS_FAILURE();
386 }
387 if (!ots::ParseCoverageTable(data + offset_coverage,
388 length - offset_coverage, num_glyphs)) {
389 return OTS_FAILURE();
390 }
391
392 return true;
393}
394
395bool ParseSubstLookupRecord(ots::Buffer *subtable, const uint16_t num_glyphs,
396 const uint16_t num_lookups) {
397 uint16_t sequence_index = 0;
398 uint16_t lookup_list_index = 0;
399
400 if (!subtable->ReadU16(&sequence_index) ||
401 !subtable->ReadU16(&lookup_list_index)) {
402 return OTS_FAILURE();
403 }
404 if (sequence_index >= num_glyphs) {
405 return OTS_FAILURE();
406 }
407 if (lookup_list_index >= num_lookups) {
408 return OTS_FAILURE();
409 }
410 return true;
411}
412
413// Lookup Type 5:
414// Contextual Substitution Subtable
415bool ParseContextSubstitution(const ots::OpenTypeFile *file,
416 const uint8_t *data, const size_t length) {
417 return ots::ParseContextSubtable(data, length, file->maxp->num_glyphs,
418 file->gsub->num_lookups);
419}
420
421// Lookup Type 6:
422// Chaining Contextual Substitution Subtable
423bool ParseChainingContextSubstitution(const ots::OpenTypeFile *file,
424 const uint8_t *data,
425 const size_t length) {
426 return ots::ParseChainingContextSubtable(data, length,
427 file->maxp->num_glyphs,
428 file->gsub->num_lookups);
429}
430
431// Lookup Type 7:
432// Extension Substition
433bool ParseExtensionSubstitution(const ots::OpenTypeFile *file,
434 const uint8_t *data, const size_t length) {
435 return ots::ParseExtensionSubtable(file, data, length,
436 &kGsubLookupSubtableParser);
437}
438
439// Lookup Type 8:
440// Reverse Chaining Contexual Single Substitution Subtable
441bool ParseReverseChainingContextSingleSubstitution(
442 const ots::OpenTypeFile *file, const uint8_t *data, const size_t length) {
443 ots::Buffer subtable(data, length);
444
445 uint16_t format = 0;
446 uint16_t offset_coverage = 0;
447
448 if (!subtable.ReadU16(&format) ||
449 !subtable.ReadU16(&offset_coverage)) {
450 return OTS_FAILURE();
451 }
452
453 const uint16_t num_glyphs = file->maxp->num_glyphs;
454
455 uint16_t backtrack_glyph_count = 0;
456 if (!subtable.ReadU16(&backtrack_glyph_count)) {
457 return OTS_FAILURE();
458 }
459 if (backtrack_glyph_count > num_glyphs) {
460 return OTS_FAILURE();
461 }
462 std::vector<uint16_t> offsets_backtrack;
463 offsets_backtrack.reserve(backtrack_glyph_count);
464 for (unsigned i = 0; i < backtrack_glyph_count; ++i) {
465 uint16_t offset = 0;
466 if (!subtable.ReadU16(&offset)) {
467 return OTS_FAILURE();
468 }
469 offsets_backtrack.push_back(offset);
470 }
471
472 uint16_t lookahead_glyph_count = 0;
473 if (!subtable.ReadU16(&lookahead_glyph_count)) {
474 return OTS_FAILURE();
475 }
476 if (lookahead_glyph_count > num_glyphs) {
477 return OTS_FAILURE();
478 }
479 std::vector<uint16_t> offsets_lookahead;
480 offsets_lookahead.reserve(lookahead_glyph_count);
481 for (unsigned i = 0; i < lookahead_glyph_count; ++i) {
482 uint16_t offset = 0;
483 if (!subtable.ReadU16(&offset)) {
484 return OTS_FAILURE();
485 }
486 offsets_lookahead.push_back(offset);
487 }
488
489 uint16_t glyph_count = 0;
490 if (!subtable.ReadU16(&glyph_count)) {
491 return OTS_FAILURE();
492 }
493 if (glyph_count > num_glyphs) {
494 return OTS_FAILURE();
495 }
496 for (unsigned i = 0; i < glyph_count; ++i) {
497 uint16_t substitute = 0;
498 if (!subtable.ReadU16(&substitute)) {
499 return OTS_FAILURE();
500 }
501 if (substitute >= num_glyphs) {
502 return OTS_FAILURE();
503 }
504 }
505
506 const unsigned substitute_end = static_cast<unsigned>(10) +
507 (backtrack_glyph_count + lookahead_glyph_count + glyph_count) * 2;
508 if (substitute_end > std::numeric_limits<uint16_t>::max()) {
509 return OTS_FAILURE();
510 }
511
512 if (offset_coverage < substitute_end || offset_coverage >= length) {
513 return OTS_FAILURE();
514 }
515 if (!ots::ParseCoverageTable(data + offset_coverage,
516 length - offset_coverage, num_glyphs)) {
517 return OTS_FAILURE();
518 }
519
520 for (unsigned i = 0; i < backtrack_glyph_count; ++i) {
521 if (offsets_backtrack[i] < substitute_end ||
522 offsets_backtrack[i] >= length) {
523 return OTS_FAILURE();
524 }
525 if (!ots::ParseCoverageTable(data + offsets_backtrack[i],
526 length - offsets_backtrack[i], num_glyphs)) {
527 return OTS_FAILURE();
528 }
529 }
530
531 for (unsigned i = 0; i < lookahead_glyph_count; ++i) {
532 if (offsets_lookahead[i] < substitute_end ||
533 offsets_lookahead[i] >= length) {
534 return OTS_FAILURE();
535 }
536 if (!ots::ParseCoverageTable(data + offsets_lookahead[i],
537 length - offsets_lookahead[i], num_glyphs)) {
538 return OTS_FAILURE();
539 }
540 }
541
542 return true;
543}
544
545} // namespace
546
547#define DROP_THIS_TABLE \
548 do { file->gsub->data = 0; file->gsub->length = 0; } while (0)
549
550namespace ots {
551
552// As far as I checked, following fonts contain invalid values in GSUB table.
553// OTS will drop their GSUB table.
554//
555// # too large substitute (value is 0xFFFF)
556// kaiu.ttf
557// mingliub2.ttf
558// mingliub1.ttf
559// mingliub0.ttf
560// GraublauWeb.otf
561// GraublauWebBold.otf
562//
563// # too large alternate (value is 0xFFFF)
564// ManchuFont.ttf
565//
566// # bad offset to lang sys table (NULL offset)
567// DejaVuMonoSansBold.ttf
568// DejaVuMonoSansBoldOblique.ttf
569// DejaVuMonoSansOblique.ttf
570// DejaVuSansMono-BoldOblique.ttf
571// DejaVuSansMono-Oblique.ttf
572// DejaVuSansMono-Bold.ttf
573//
574// # bad start coverage index
575// GenBasBI.ttf
576// GenBasI.ttf
577// AndBasR.ttf
578// GenBkBasI.ttf
579// CharisSILR.ttf
580// CharisSILBI.ttf
581// CharisSILI.ttf
582// CharisSILB.ttf
583// DoulosSILR.ttf
584// CharisSILBI.ttf
585// GenBkBasB.ttf
586// GenBkBasR.ttf
587// GenBkBasBI.ttf
588// GenBasB.ttf
589// GenBasR.ttf
590//
591// # glyph range is overlapping
592// KacstTitleL.ttf
593// KacstDecorative.ttf
594// KacstTitle.ttf
595// KacstArt.ttf
596// KacstPoster.ttf
597// KacstQurn.ttf
598// KacstDigital.ttf
599// KacstBook.ttf
600// KacstFarsi.ttf
601
602bool ots_gsub_parse(OpenTypeFile *file, const uint8_t *data, size_t length) {
603 // Parsing gsub table requires |file->maxp->num_glyphs|
604 if (!file->maxp) {
605 return OTS_FAILURE();
606 }
607
608 Buffer table(data, length);
609
610 OpenTypeGSUB *gsub = new OpenTypeGSUB;
611 file->gsub = gsub;
612
613 uint32_t version = 0;
614 uint16_t offset_script_list = 0;
615 uint16_t offset_feature_list = 0;
616 uint16_t offset_lookup_list = 0;
617 if (!table.ReadU32(&version) ||
618 !table.ReadU16(&offset_script_list) ||
619 !table.ReadU16(&offset_feature_list) ||
620 !table.ReadU16(&offset_lookup_list)) {
621 return OTS_FAILURE();
622 }
623
624 if (version != 0x00010000) {
625 OTS_WARNING("bad GSUB version");
626 DROP_THIS_TABLE;
627 return true;
628 }
629 if ((offset_script_list < kGsubHeaderSize ||
630 offset_script_list >= length) ||
631 (offset_feature_list < kGsubHeaderSize ||
632 offset_feature_list >= length) ||
633 (offset_lookup_list < kGsubHeaderSize ||
634 offset_lookup_list >= length)) {
635 OTS_WARNING("bad offset in GSUB header");
636 DROP_THIS_TABLE;
637 return true;
638 }
639
640 if (!ParseLookupListTable(file, data + offset_lookup_list,
641 length - offset_lookup_list,
642 &kGsubLookupSubtableParser,
643 &gsub->num_lookups)) {
644 OTS_WARNING("faild to parse lookup list table");
645 DROP_THIS_TABLE;
646 return true;
647 }
648
649 uint16_t num_features = 0;
650 if (!ParseFeatureListTable(data + offset_feature_list,
651 length - offset_feature_list, gsub->num_lookups,
652 &num_features)) {
653 OTS_WARNING("faild to parse feature list table");
654 DROP_THIS_TABLE;
655 return true;
656 }
657
658 if (!ParseScriptListTable(data + offset_script_list,
659 length - offset_script_list, num_features)) {
660 OTS_WARNING("faild to parse script list table");
661 DROP_THIS_TABLE;
662 return true;
663 }
664
665 gsub->data = data;
666 gsub->length = length;
667 return true;
668}
669
670bool ots_gsub_should_serialise(OpenTypeFile *file) {
671 const bool needed_tables_dropped =
672 (file->gdef && file->gdef->data == NULL) ||
673 (file->gpos && file->gpos->data == NULL);
674 return file->gsub != NULL && file->gsub->data != NULL
675 && !needed_tables_dropped;
676}
677
678bool ots_gsub_serialise(OTSStream *out, OpenTypeFile *file) {
679 if (!out->Write(file->gsub->data, file->gsub->length)) {
680 return OTS_FAILURE();
681 }
682
683 return true;
684}
685
686void ots_gsub_free(OpenTypeFile *file) {
687 delete file->gsub;
688}
689
690} // namespace ots
691