-
Notifications
You must be signed in to change notification settings - Fork 448
/
__init__.py
3132 lines (2785 loc) · 106 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2013 Google, Inc. All Rights Reserved.
#
# Google Author(s): Behdad Esfahbod
from fontTools.misc.roundTools import otRound
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
from fontTools.otlLib.maxContextCalc import maxCtxFont
from fontTools.pens.basePen import NullPen
from fontTools.misc.loggingTools import Timer
from fontTools.subset.cff import *
import sys
import struct
import array
import logging
from collections import Counter, defaultdict
from functools import reduce
from types import MethodType
__usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
__doc__="""\
pyftsubset -- OpenType font subsetter and optimizer
pyftsubset is an OpenType font subsetter and optimizer, based on fontTools.
It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff)
font file. The subsetted glyph set is based on the specified glyphs
or characters, and specified OpenType layout features.
The tool also performs some size-reducing optimizations, aimed for using
subset fonts as webfonts. Individual optimizations can be enabled or
disabled, and are enabled by default when they are safe.
Usage:
"""+__usage__+"""
At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file,
--text, --text-file, --unicodes, or --unicodes-file, must be specified.
Arguments:
font-file
The input font file.
glyph
Specify one or more glyph identifiers to include in the subset. Must be
PS glyph names, or the special string '*' to keep the entire glyph set.
Initial glyph set specification:
These options populate the initial glyph set. Same option can appear
multiple times, and the results are accummulated.
--gids=<NNN>[,<NNN>...]
Specify comma/whitespace-separated list of glyph IDs or ranges as
decimal numbers. For example, --gids=10-12,14 adds glyphs with
numbers 10, 11, 12, and 14.
--gids-file=<path>
Like --gids but reads from a file. Anything after a '#' on any line
is ignored as comments.
--glyphs=<glyphname>[,<glyphname>...]
Specify comma/whitespace-separated PS glyph names to add to the subset.
Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc
that are accepted on the command line. The special string '*' will keep
the entire glyph set.
--glyphs-file=<path>
Like --glyphs but reads from a file. Anything after a '#' on any line
is ignored as comments.
--text=<text>
Specify characters to include in the subset, as UTF-8 string.
--text-file=<path>
Like --text but reads from a file. Newline character are not added to
the subset.
--unicodes=<XXXX>[,<XXXX>...]
Specify comma/whitespace-separated list of Unicode codepoints or
ranges as hex numbers, optionally prefixed with 'U+', 'u', etc.
For example, --unicodes=41-5a,61-7a adds ASCII letters, so does
the more verbose --unicodes=U+0041-005A,U+0061-007A.
The special strings '*' will choose all Unicode characters mapped
by the font.
--unicodes-file=<path>
Like --unicodes, but reads from a file. Anything after a '#' on any
line in the file is ignored as comments.
--ignore-missing-glyphs
Do not fail if some requested glyphs or gids are not available in
the font.
--no-ignore-missing-glyphs
Stop and fail if some requested glyphs or gids are not available
in the font. [default]
--ignore-missing-unicodes [default]
Do not fail if some requested Unicode characters (including those
indirectly specified using --text or --text-file) are not available
in the font.
--no-ignore-missing-unicodes
Stop and fail if some requested Unicode characters are not available
in the font.
Note the default discrepancy between ignoring missing glyphs versus
unicodes. This is for historical reasons and in the future
--no-ignore-missing-unicodes might become default.
Other options:
For the other options listed below, to see the current value of the option,
pass a value of '?' to it, with or without a '='.
Examples:
$ pyftsubset --glyph-names?
Current setting for 'glyph-names' is: False
$ ./pyftsubset --name-IDs=?
Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
$ ./pyftsubset --hinting? --no-hinting --hinting?
Current setting for 'hinting' is: True
Current setting for 'hinting' is: False
Output options:
--output-file=<path>
The output font file. If not specified, the subsetted font
will be saved in as font-file.subset.
--flavor=<type>
Specify flavor of output font file. May be 'woff' or 'woff2'.
Note that WOFF2 requires the Brotli Python extension, available
at https://github.com/google/brotli
--with-zopfli
Use the Google Zopfli algorithm to compress WOFF. The output is 3-8 %
smaller than pure zlib, but the compression speed is much slower.
The Zopfli Python bindings are available at:
https://pypi.python.org/pypi/zopfli
Glyph set expansion:
These options control how additional glyphs are added to the subset.
--retain-gids
Retain glyph indices; just empty glyphs not needed in-place.
--notdef-glyph
Add the '.notdef' glyph to the subset (ie, keep it). [default]
--no-notdef-glyph
Drop the '.notdef' glyph unless specified in the glyph set. This
saves a few bytes, but is not possible for Postscript-flavored
fonts, as those require '.notdef'. For TrueType-flavored fonts,
this works fine as long as no unsupported glyphs are requested
from the font.
--notdef-outline
Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is
used when glyphs not supported by the font are to be shown. It is not
needed otherwise.
--no-notdef-outline
When including a '.notdef' glyph, remove its outline. This saves
a few bytes. [default]
--recommended-glyphs
Add glyphs 0, 1, 2, and 3 to the subset, as recommended for
TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'.
Some legacy software might require this, but no modern system does.
--no-recommended-glyphs
Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
glyph set. [default]
--no-layout-closure
Do not expand glyph set to add glyphs produced by OpenType layout
features. Instead, OpenType layout features will be subset to only
rules that are relevant to the otherwise-specified glyph set.
--layout-features[+|-]=<feature>[,<feature>...]
Specify (=), add to (+=) or exclude from (-=) the comma-separated
set of OpenType layout feature tags that will be preserved.
Glyph variants used by the preserved features are added to the
specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs',
'dnom', 'frac', 'kern', 'liga', 'locl', 'mark', 'mkmk', 'numr', 'rclt',
'rlig', 'rvrn', and all features required for script shaping are
preserved. To see the full list, try '--layout-features=?'.
Use '*' to keep all features.
Multiple --layout-features options can be provided if necessary.
Examples:
--layout-features+=onum,pnum,ss01
* Keep the default set of features and 'onum', 'pnum', 'ss01'.
--layout-features-='mark','mkmk'
* Keep the default set of features but drop 'mark' and 'mkmk'.
--layout-features='kern'
* Only keep the 'kern' feature, drop all others.
--layout-features=''
* Drop all features.
--layout-features='*'
* Keep all features.
--layout-features+=aalt --layout-features-=vrt2
* Keep default set of features plus 'aalt', but drop 'vrt2'.
--layout-scripts[+|-]=<script>[,<script>...]
Specify (=), add to (+=) or exclude from (-=) the comma-separated
set of OpenType layout script tags that will be preserved. LangSys tags
can be appended to script tag, separated by '.', for example:
'arab.dflt,arab.URD,latn.TRK'. By default all scripts are retained ('*').
Hinting options:
--hinting
Keep hinting [default]
--no-hinting
Drop glyph-specific hinting and font-wide hinting tables, as well
as remove hinting-related bits and pieces from other tables (eg. GPOS).
See --hinting-tables for list of tables that are dropped by default.
Instructions and hints are stripped from 'glyf' and 'CFF ' tables
respectively. This produces (sometimes up to 30%) smaller fonts that
are suitable for extremely high-resolution systems, like high-end
mobile devices and retina displays.
Optimization options:
--desubroutinize
Remove CFF use of subroutinizes. Subroutinization is a way to make CFF
fonts smaller. For small subsets however, desubroutinizing might make
the font smaller. It has even been reported that desubroutinized CFF
fonts compress better (produce smaller output) WOFF and WOFF2 fonts.
Also see note under --no-hinting.
--no-desubroutinize [default]
Leave CFF subroutinizes as is, only throw away unused subroutinizes.
Font table options:
--drop-tables[+|-]=<table>[,<table>...]
Specify (=), add to (+=) or exclude from (-=) the comma-separated
set of tables that will be be dropped.
By default, the following tables are dropped:
'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', 'PCLT', 'LTSH'
and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'.
The tool will attempt to subset the remaining tables.
Examples:
--drop-tables-='SVG '
* Drop the default set of tables but keep 'SVG '.
--drop-tables+=GSUB
* Drop the default set of tables and 'GSUB'.
--drop-tables=DSIG
* Only drop the 'DSIG' table, keep all others.
--drop-tables=
* Keep all tables.
--no-subset-tables+=<table>[,<table>...]
Add to the set of tables that will not be subsetted.
By default, the following tables are included in this list, as
they do not need subsetting (ignore the fact that 'loca' is listed
here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name',
'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'.
By default, tables that the tool does not know how to subset and are not
specified here will be dropped from the font, unless --passthrough-tables
option is passed.
Example:
--no-subset-tables+=FFTM
* Keep 'FFTM' table in the font by preventing subsetting.
--passthrough-tables
Do not drop tables that the tool does not know how to subset.
--no-passthrough-tables
Tables that the tool does not know how to subset and are not specified
in --no-subset-tables will be dropped from the font. [default]
--hinting-tables[-]=<table>[,<table>...]
Specify (=), add to (+=) or exclude from (-=) the list of font-wide
hinting tables that will be dropped if --no-hinting is specified,
Examples:
--hinting-tables-='VDMX'
* Drop font-wide hinting tables except 'VDMX'.
--hinting-tables=''
* Keep all font-wide hinting tables (but strip hints from glyphs).
--legacy-kern
Keep TrueType 'kern' table even when OpenType 'GPOS' is available.
--no-legacy-kern
Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default]
Font naming options:
These options control what is retained in the 'name' table. For numerical
codes, see: http://www.microsoft.com/typography/otspec/name.htm
--name-IDs[+|-]=<nameID>[,<nameID>...]
Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
entry nameIDs that will be preserved. By default, only nameIDs between 0
and 6 are preserved, the rest are dropped. Use '*' to keep all entries.
Examples:
--name-IDs+=7,8,9
* Also keep Trademark, Manufacturer and Designer name entries.
--name-IDs=''
* Drop all 'name' table entries.
--name-IDs='*'
* keep all 'name' table entries
--name-legacy
Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.).
XXX Note: This might be needed for some fonts that have no Unicode name
entires for English. See: https://github.com/fonttools/fonttools/issues/146
--no-name-legacy
Drop legacy (non-Unicode) 'name' table entries [default]
--name-languages[+|-]=<langID>[,<langID>]
Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
langIDs that will be preserved. By default only records with langID
0x0409 (English) are preserved. Use '*' to keep all langIDs.
--obfuscate-names
Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4,
and 6 with dummy strings (it is still fully functional as webfont).
Glyph naming and encoding options:
--glyph-names
Keep PS glyph names in TT-flavored fonts. In general glyph names are
not needed for correct use of the font. However, some PDF generators
and PDF viewers might rely on glyph names to extract Unicode text
from PDF documents.
--no-glyph-names
Drop PS glyph names in TT-flavored fonts, by using 'post' table
version 3.0. [default]
--legacy-cmap
Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.).
--no-legacy-cmap
Drop the legacy 'cmap' subtables. [default]
--symbol-cmap
Keep the 3.0 symbol 'cmap'.
--no-symbol-cmap
Drop the 3.0 symbol 'cmap'. [default]
Other font-specific options:
--recalc-bounds
Recalculate font bounding boxes.
--no-recalc-bounds
Keep original font bounding boxes. This is faster and still safe
for all practical purposes. [default]
--recalc-timestamp
Set font 'modified' timestamp to current time.
--no-recalc-timestamp
Do not modify font 'modified' timestamp. [default]
--canonical-order
Order tables as recommended in the OpenType standard. This is not
required by the standard, nor by any known implementation.
--no-canonical-order
Keep original order of font tables. This is faster. [default]
--prune-unicode-ranges
Update the 'OS/2 ulUnicodeRange*' bits after subsetting. The Unicode
ranges defined in the OpenType specification v1.7 are intersected with
the Unicode codepoints specified in the font's Unicode 'cmap' subtables:
when no overlap is found, the bit will be switched off. However, it will
*not* be switched on if an intersection is found. [default]
--no-prune-unicode-ranges
Don't change the 'OS/2 ulUnicodeRange*' bits.
--recalc-average-width
Update the 'OS/2 xAvgCharWidth' field after subsetting.
--no-recalc-average-width
Don't change the 'OS/2 xAvgCharWidth' field. [default]
--recalc-max-context
Update the 'OS/2 usMaxContext' field after subsetting.
--no-recalc-max-context
Don't change the 'OS/2 usMaxContext' field. [default]
--font-number=<number>
Select font number for TrueType Collection (.ttc/.otc), starting from 0.
Application options:
--verbose
Display verbose information of the subsetting process.
--timing
Display detailed timing information of the subsetting process.
--xml
Display the TTX XML representation of subsetted font.
Example:
Produce a subset containing the characters ' !"#$%' without performing
size-reducing optimizations:
$ pyftsubset font.ttf --unicodes="U+0020-0025" \\
--layout-features='*' --glyph-names --symbol-cmap --legacy-cmap \\
--notdef-glyph --notdef-outline --recommended-glyphs \\
--name-IDs='*' --name-legacy --name-languages='*'
"""
log = logging.getLogger("fontTools.subset")
def _log_glyphs(self, glyphs, font=None):
self.info("Glyph names: %s", sorted(glyphs))
if font:
reverseGlyphMap = font.getReverseGlyphMap()
self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs))
# bind "glyphs" function to 'log' object
log.glyphs = MethodType(_log_glyphs, log)
# I use a different timing channel so I can configure it separately from the
# main module's logger
timer = Timer(logger=logging.getLogger("fontTools.subset.timer"))
def _add_method(*clazzes):
"""Returns a decorator function that adds a new method to one or
more classes."""
def wrapper(method):
done = []
for clazz in clazzes:
if clazz in done: continue # Support multiple names of a clazz
done.append(clazz)
assert clazz.__name__ != 'DefaultTable', \
'Oops, table class not found.'
assert not hasattr(clazz, method.__name__), \
"Oops, class '%s' has method '%s'." % (clazz.__name__,
method.__name__)
setattr(clazz, method.__name__, method)
return None
return wrapper
def _uniq_sort(l):
return sorted(set(l))
def _dict_subset(d, glyphs):
return {g:d[g] for g in glyphs}
def _list_subset(l, indices):
count = len(l)
return [l[i] for i in indices if i < count]
@_add_method(otTables.Coverage)
def intersect(self, glyphs):
"""Returns ascending list of matching coverage values."""
return [i for i,g in enumerate(self.glyphs) if g in glyphs]
@_add_method(otTables.Coverage)
def intersect_glyphs(self, glyphs):
"""Returns set of intersecting glyphs."""
return set(g for g in self.glyphs if g in glyphs)
@_add_method(otTables.Coverage)
def subset(self, glyphs):
"""Returns ascending list of remaining coverage values."""
indices = self.intersect(glyphs)
self.glyphs = [g for g in self.glyphs if g in glyphs]
return indices
@_add_method(otTables.Coverage)
def remap(self, coverage_map):
"""Remaps coverage."""
self.glyphs = [self.glyphs[i] for i in coverage_map]
@_add_method(otTables.ClassDef)
def intersect(self, glyphs):
"""Returns ascending list of matching class values."""
return _uniq_sort(
([0] if any(g not in self.classDefs for g in glyphs) else []) +
[v for g,v in self.classDefs.items() if g in glyphs])
@_add_method(otTables.ClassDef)
def intersect_class(self, glyphs, klass):
"""Returns set of glyphs matching class."""
if klass == 0:
return set(g for g in glyphs if g not in self.classDefs)
return set(g for g,v in self.classDefs.items()
if v == klass and g in glyphs)
@_add_method(otTables.ClassDef)
def subset(self, glyphs, remap=False, useClass0=True):
"""Returns ascending list of remaining classes."""
self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs}
# Note: while class 0 has the special meaning of "not matched",
# if no glyph will ever /not match/, we can optimize class 0 out too.
# Only do this if allowed.
indices = _uniq_sort(
([0] if ((not useClass0) or any(g not in self.classDefs for g in glyphs)) else []) +
list(self.classDefs.values()))
if remap:
self.remap(indices)
return indices
@_add_method(otTables.ClassDef)
def remap(self, class_map):
"""Remaps classes."""
self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()}
@_add_method(otTables.SingleSubst)
def closure_glyphs(self, s, cur_glyphs):
s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs)
@_add_method(otTables.SingleSubst)
def subset_glyphs(self, s):
self.mapping = {g:v for g,v in self.mapping.items()
if g in s.glyphs and v in s.glyphs}
return bool(self.mapping)
@_add_method(otTables.MultipleSubst)
def closure_glyphs(self, s, cur_glyphs):
for glyph, subst in self.mapping.items():
if glyph in cur_glyphs:
s.glyphs.update(subst)
@_add_method(otTables.MultipleSubst)
def subset_glyphs(self, s):
self.mapping = {g:v for g,v in self.mapping.items()
if g in s.glyphs and all(sub in s.glyphs for sub in v)}
return bool(self.mapping)
@_add_method(otTables.AlternateSubst)
def closure_glyphs(self, s, cur_glyphs):
s.glyphs.update(*(vlist for g,vlist in self.alternates.items()
if g in cur_glyphs))
@_add_method(otTables.AlternateSubst)
def subset_glyphs(self, s):
self.alternates = {g:[v for v in vlist if v in s.glyphs]
for g,vlist in self.alternates.items()
if g in s.glyphs and
any(v in s.glyphs for v in vlist)}
return bool(self.alternates)
@_add_method(otTables.LigatureSubst)
def closure_glyphs(self, s, cur_glyphs):
s.glyphs.update(*([seq.LigGlyph for seq in seqs
if all(c in s.glyphs for c in seq.Component)]
for g,seqs in self.ligatures.items()
if g in cur_glyphs))
@_add_method(otTables.LigatureSubst)
def subset_glyphs(self, s):
self.ligatures = {g:v for g,v in self.ligatures.items()
if g in s.glyphs}
self.ligatures = {g:[seq for seq in seqs
if seq.LigGlyph in s.glyphs and
all(c in s.glyphs for c in seq.Component)]
for g,seqs in self.ligatures.items()}
self.ligatures = {g:v for g,v in self.ligatures.items() if v}
return bool(self.ligatures)
@_add_method(otTables.ReverseChainSingleSubst)
def closure_glyphs(self, s, cur_glyphs):
if self.Format == 1:
indices = self.Coverage.intersect(cur_glyphs)
if(not indices or
not all(c.intersect(s.glyphs)
for c in self.LookAheadCoverage + self.BacktrackCoverage)):
return
s.glyphs.update(self.Substitute[i] for i in indices)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ReverseChainSingleSubst)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
self.Substitute = _list_subset(self.Substitute, indices)
# Now drop rules generating glyphs we don't want
indices = [i for i,sub in enumerate(self.Substitute)
if sub in s.glyphs]
self.Substitute = _list_subset(self.Substitute, indices)
self.Coverage.remap(indices)
self.GlyphCount = len(self.Substitute)
return bool(self.GlyphCount and
all(c.subset(s.glyphs)
for c in self.LookAheadCoverage+self.BacktrackCoverage))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.Device)
def is_hinting(self):
return self.DeltaFormat in (1,2,3)
@_add_method(otTables.ValueRecord)
def prune_hints(self):
for name in ['XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice']:
v = getattr(self, name, None)
if v is not None and v.is_hinting():
delattr(self, name)
@_add_method(otTables.SinglePos)
def subset_glyphs(self, s):
if self.Format == 1:
return len(self.Coverage.subset(s.glyphs))
elif self.Format == 2:
indices = self.Coverage.subset(s.glyphs)
values = self.Value
count = len(values)
self.Value = [values[i] for i in indices if i < count]
self.ValueCount = len(self.Value)
return bool(self.ValueCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.SinglePos)
def prune_post_subset(self, font, options):
if self.Value is None:
assert self.ValueFormat == 0
return True
# Shrink ValueFormat
if self.Format == 1:
if not options.hinting:
self.Value.prune_hints()
self.ValueFormat = self.Value.getEffectiveFormat()
elif self.Format == 2:
if not options.hinting:
for v in self.Value:
v.prune_hints()
self.ValueFormat = reduce(int.__or__, [v.getEffectiveFormat() for v in self.Value], 0)
# Downgrade to Format 1 if all ValueRecords are the same
if self.Format == 2 and all(v == self.Value[0] for v in self.Value):
self.Format = 1
self.Value = self.Value[0] if self.ValueFormat != 0 else None
del self.ValueCount
return True
@_add_method(otTables.PairPos)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
pairs = self.PairSet
count = len(pairs)
self.PairSet = [pairs[i] for i in indices if i < count]
for p in self.PairSet:
p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs]
p.PairValueCount = len(p.PairValueRecord)
# Remove empty pairsets
indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount]
self.Coverage.remap(indices)
self.PairSet = _list_subset(self.PairSet, indices)
self.PairSetCount = len(self.PairSet)
return bool(self.PairSetCount)
elif self.Format == 2:
class1_map = [c for c in self.ClassDef1.subset(s.glyphs.intersection(self.Coverage.glyphs), remap=True) if c < self.Class1Count]
class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True, useClass0=False) if c < self.Class2Count]
self.Class1Record = [self.Class1Record[i] for i in class1_map]
for c in self.Class1Record:
c.Class2Record = [c.Class2Record[i] for i in class2_map]
self.Class1Count = len(class1_map)
self.Class2Count = len(class2_map)
# If only Class2 0 left, no need to keep anything.
return bool(self.Class1Count and
(self.Class2Count > 1) and
self.Coverage.subset(s.glyphs))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.PairPos)
def prune_post_subset(self, font, options):
if not options.hinting:
attr1, attr2 = {
1: ('PairSet', 'PairValueRecord'),
2: ('Class1Record', 'Class2Record'),
}[self.Format]
self.ValueFormat1 = self.ValueFormat2 = 0
for row in getattr(self, attr1):
for r in getattr(row, attr2):
if r.Value1:
r.Value1.prune_hints()
self.ValueFormat1 |= r.Value1.getEffectiveFormat()
if r.Value2:
r.Value2.prune_hints()
self.ValueFormat2 |= r.Value2.getEffectiveFormat()
return bool(self.ValueFormat1 | self.ValueFormat2)
@_add_method(otTables.CursivePos)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
records = self.EntryExitRecord
count = len(records)
self.EntryExitRecord = [records[i] for i in indices if i < count]
self.EntryExitCount = len(self.EntryExitRecord)
return bool(self.EntryExitCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.Anchor)
def prune_hints(self):
if self.Format == 2:
self.Format = 1
elif self.Format == 3:
for name in ('XDeviceTable', 'YDeviceTable'):
v = getattr(self, name, None)
if v is not None and v.is_hinting():
setattr(self, name, None)
if self.XDeviceTable is None and self.YDeviceTable is None:
self.Format = 1
@_add_method(otTables.CursivePos)
def prune_post_subset(self, font, options):
if not options.hinting:
for rec in self.EntryExitRecord:
if rec.EntryAnchor: rec.EntryAnchor.prune_hints()
if rec.ExitAnchor: rec.ExitAnchor.prune_hints()
return True
@_add_method(otTables.MarkBasePos)
def subset_glyphs(self, s):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset(s.glyphs)
self.MarkArray.MarkRecord = _list_subset(self.MarkArray.MarkRecord, mark_indices)
self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
base_indices = self.BaseCoverage.subset(s.glyphs)
self.BaseArray.BaseRecord = _list_subset(self.BaseArray.BaseRecord, base_indices)
self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.MarkArray.MarkRecord:
m.Class = class_indices.index(m.Class)
for b in self.BaseArray.BaseRecord:
b.BaseAnchor = _list_subset(b.BaseAnchor, class_indices)
return bool(self.ClassCount and
self.MarkArray.MarkCount and
self.BaseArray.BaseCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkBasePos)
def prune_post_subset(self, font, options):
if not options.hinting:
for m in self.MarkArray.MarkRecord:
if m.MarkAnchor:
m.MarkAnchor.prune_hints()
for b in self.BaseArray.BaseRecord:
for a in b.BaseAnchor:
if a:
a.prune_hints()
return True
@_add_method(otTables.MarkLigPos)
def subset_glyphs(self, s):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset(s.glyphs)
self.MarkArray.MarkRecord = _list_subset(self.MarkArray.MarkRecord, mark_indices)
self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
ligature_indices = self.LigatureCoverage.subset(s.glyphs)
self.LigatureArray.LigatureAttach = _list_subset(self.LigatureArray.LigatureAttach, ligature_indices)
self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.MarkArray.MarkRecord:
m.Class = class_indices.index(m.Class)
for l in self.LigatureArray.LigatureAttach:
for c in l.ComponentRecord:
c.LigatureAnchor = _list_subset(c.LigatureAnchor, class_indices)
return bool(self.ClassCount and
self.MarkArray.MarkCount and
self.LigatureArray.LigatureCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkLigPos)
def prune_post_subset(self, font, options):
if not options.hinting:
for m in self.MarkArray.MarkRecord:
if m.MarkAnchor:
m.MarkAnchor.prune_hints()
for l in self.LigatureArray.LigatureAttach:
for c in l.ComponentRecord:
for a in c.LigatureAnchor:
if a:
a.prune_hints()
return True
@_add_method(otTables.MarkMarkPos)
def subset_glyphs(self, s):
if self.Format == 1:
mark1_indices = self.Mark1Coverage.subset(s.glyphs)
self.Mark1Array.MarkRecord = _list_subset(self.Mark1Array.MarkRecord, mark1_indices)
self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
mark2_indices = self.Mark2Coverage.subset(s.glyphs)
self.Mark2Array.Mark2Record = _list_subset(self.Mark2Array.Mark2Record, mark2_indices)
self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.Mark1Array.MarkRecord:
m.Class = class_indices.index(m.Class)
for b in self.Mark2Array.Mark2Record:
b.Mark2Anchor = _list_subset(b.Mark2Anchor, class_indices)
return bool(self.ClassCount and
self.Mark1Array.MarkCount and
self.Mark2Array.MarkCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkMarkPos)
def prune_post_subset(self, font, options):
if not options.hinting:
for m in self.Mark1Array.MarkRecord:
if m.MarkAnchor:
m.MarkAnchor.prune_hints()
for b in self.Mark2Array.Mark2Record:
for m in b.Mark2Anchor:
if m:
m.prune_hints()
return True
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.SinglePos,
otTables.PairPos,
otTables.CursivePos,
otTables.MarkBasePos,
otTables.MarkLigPos,
otTables.MarkMarkPos)
def subset_lookups(self, lookup_indices):
pass
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.SinglePos,
otTables.PairPos,
otTables.CursivePos,
otTables.MarkBasePos,
otTables.MarkLigPos,
otTables.MarkMarkPos)
def collect_lookups(self):
return []
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def prune_post_subset(self, font, options):
return True
@_add_method(otTables.SingleSubst,
otTables.AlternateSubst,
otTables.ReverseChainSingleSubst)
def may_have_non_1to1(self):
return False
@_add_method(otTables.MultipleSubst,
otTables.LigatureSubst,
otTables.ContextSubst,
otTables.ChainContextSubst)
def may_have_non_1to1(self):
return True
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def __subset_classify_context(self):
class ContextHelper(object):
def __init__(self, klass, Format):
if klass.__name__.endswith('Subst'):
Typ = 'Sub'
Type = 'Subst'
else:
Typ = 'Pos'
Type = 'Pos'
if klass.__name__.startswith('Chain'):
Chain = 'Chain'
InputIdx = 1
DataLen = 3
else:
Chain = ''
InputIdx = 0
DataLen = 1
ChainTyp = Chain+Typ
self.Typ = Typ
self.Type = Type
self.Chain = Chain
self.ChainTyp = ChainTyp
self.InputIdx = InputIdx
self.DataLen = DataLen
self.LookupRecord = Type+'LookupRecord'
if Format == 1:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(None,)
ChainContextData = lambda r:(None, None, None)
SetContextData = None
SetChainContextData = None
RuleData = lambda r:(r.Input,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
def SetRuleData(r, d):
(r.Input,) = d
(r.GlyphCount,) = (len(x)+1 for x in d)
def ChainSetRuleData(r, d):
(r.Backtrack, r.Input, r.LookAhead) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2]))
elif Format == 2:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(r.ClassDef,)
ChainContextData = lambda r:(r.BacktrackClassDef,
r.InputClassDef,
r.LookAheadClassDef)
def SetContextData(r, d):
(r.ClassDef,) = d
def SetChainContextData(r, d):
(r.BacktrackClassDef,
r.InputClassDef,
r.LookAheadClassDef) = d
RuleData = lambda r:(r.Class,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
def SetRuleData(r, d):
(r.Class,) = d
(r.GlyphCount,) = (len(x)+1 for x in d)
def ChainSetRuleData(r, d):
(r.Backtrack, r.Input, r.LookAhead) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2]))
elif Format == 3:
Coverage = lambda r: r.Coverage[0]
ChainCoverage = lambda r: r.InputCoverage[0]
ContextData = None
ChainContextData = None
SetContextData = None
SetChainContextData = None
RuleData = lambda r: r.Coverage
ChainRuleData = lambda r:(r.BacktrackCoverage +
r.InputCoverage +
r.LookAheadCoverage)
def SetRuleData(r, d):
(r.Coverage,) = d
(r.GlyphCount,) = (len(x) for x in d)
def ChainSetRuleData(r, d):
(r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
(r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d)
else:
assert 0, "unknown format: %s" % Format
if Chain:
self.Coverage = ChainCoverage
self.ContextData = ChainContextData
self.SetContextData = SetChainContextData
self.RuleData = ChainRuleData
self.SetRuleData = ChainSetRuleData
else:
self.Coverage = Coverage
self.ContextData = ContextData
self.SetContextData = SetContextData
self.RuleData = RuleData
self.SetRuleData = SetRuleData
if Format == 1:
self.Rule = ChainTyp+'Rule'
self.RuleCount = ChainTyp+'RuleCount'
self.RuleSet = ChainTyp+'RuleSet'
self.RuleSetCount = ChainTyp+'RuleSetCount'
self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
elif Format == 2:
self.Rule = ChainTyp+'ClassRule'
self.RuleCount = ChainTyp+'ClassRuleCount'
self.RuleSet = ChainTyp+'ClassSet'
self.RuleSetCount = ChainTyp+'ClassSetCount'
self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c
else (set(glyphs) if r == 0 else set()))
self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
self.ClassDefIndex = 1 if Chain else 0
self.Input = 'Input' if Chain else 'Class'
elif Format == 3:
self.Input = 'InputCoverage' if Chain else 'Coverage'
if self.Format not in [1, 2, 3]:
return None # Don't shoot the messenger; let it go
if not hasattr(self.__class__, "_subset__ContextHelpers"):
self.__class__._subset__ContextHelpers = {}
if self.Format not in self.__class__._subset__ContextHelpers:
helper = ContextHelper(self.__class__, self.Format)
self.__class__._subset__ContextHelpers[self.Format] = helper
return self.__class__._subset__ContextHelpers[self.Format]
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst)
def closure_glyphs(self, s, cur_glyphs):
c = self.__subset_classify_context()
indices = c.Coverage(self).intersect(cur_glyphs)
if not indices:
return []
cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs)
if self.Format == 1:
ContextData = c.ContextData(self)
rss = getattr(self, c.RuleSet)
rssCount = getattr(self, c.RuleSetCount)
for i in indices:
if i >= rssCount or not rss[i]: continue
for r in getattr(rss[i], c.Rule):
if not r: continue
if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
for cd,klist in zip(ContextData, c.RuleData(r))):
continue
chaos = set()
for ll in getattr(r, c.LookupRecord):
if not ll: continue
seqi = ll.SequenceIndex
if seqi in chaos:
# TODO Can we improve this?
pos_glyphs = None
else:
if seqi == 0:
pos_glyphs = frozenset([c.Coverage(self).glyphs[i]])
else:
pos_glyphs = frozenset([r.Input[seqi - 1]])
lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
chaos.add(seqi)
if lookup.may_have_non_1to1():
chaos.update(range(seqi, len(r.Input)+2))
lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
elif self.Format == 2:
ClassDef = getattr(self, c.ClassDef)
indices = ClassDef.intersect(cur_glyphs)
ContextData = c.ContextData(self)
rss = getattr(self, c.RuleSet)
rssCount = getattr(self, c.RuleSetCount)
for i in indices:
if i >= rssCount or not rss[i]: continue
for r in getattr(rss[i], c.Rule):
if not r: continue
if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist)