subset.py 104 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748
  1. # Copyright 2013 Google, Inc. All Rights Reserved.
  2. #
  3. # Google Author(s): Behdad Esfahbod
  4. # subset.py file from Just van Rossom's fonttools project
  5. #
  6. # Copyright 1999-2004
  7. # by Just van Rossum, Letterror, The Netherlands.
  8. # The full text of the license is available at https://github.com/behdad/fonttools/blob/master/LICENSE.txt
  9. from __future__ import print_function, division, absolute_import
  10. from fontTools.misc.py23 import *
  11. from fontTools import ttLib
  12. from fontTools.ttLib.tables import otTables
  13. from fontTools.misc import psCharStrings
  14. import sys
  15. import struct
  16. import time
  17. import array
  18. __usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
  19. __doc__="""\
  20. pyftsubset -- OpenType font subsetter and optimizer
  21. pyftsubset is an OpenType font subsetter and optimizer, based on fontTools.
  22. It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff)
  23. font file. The subsetted glyph set is based on the specified glyphs
  24. or characters, and specified OpenType layout features.
  25. The tool also performs some size-reducing optimizations, aimed for using
  26. subset fonts as webfonts. Individual optimizations can be enabled or
  27. disabled, and are enabled by default when they are safe.
  28. Usage:
  29. """+__usage__+"""
  30. At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file,
  31. --text, --text-file, --unicodes, or --unicodes-file, must be specified.
  32. Arguments:
  33. font-file
  34. The input font file.
  35. glyph
  36. Specify one or more glyph identifiers to include in the subset. Must be
  37. PS glyph names, or the special string '*' to keep the entire glyph set.
  38. Initial glyph set specification:
  39. These options populate the initial glyph set. Same option can appear
  40. multiple times, and the results are accummulated.
  41. --gids=<NNN>[,<NNN>...]
  42. Specify comma/whitespace-separated list of glyph IDs or ranges as
  43. decimal numbers. For example, --gids=10-12,14 adds glyphs with
  44. numbers 10, 11, 12, and 14.
  45. --gids-file=<path>
  46. Like --gids but reads from a file. Anything after a '#' on any line
  47. is ignored as comments.
  48. --glyphs=<glyphname>[,<glyphname>...]
  49. Specify comma/whitespace-separated PS glyph names to add to the subset.
  50. Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc
  51. that are accepted on the command line. The special string '*' wil keep
  52. the entire glyph set.
  53. --glyphs-file=<path>
  54. Like --glyphs but reads from a file. Anything after a '#' on any line
  55. is ignored as comments.
  56. --text=<text>
  57. Specify characters to include in the subset, as UTF-8 string.
  58. --text-file=<path>
  59. Like --text but reads from a file. Newline character are not added to
  60. the subset.
  61. --unicodes=<XXXX>[,<XXXX>...]
  62. Specify comma/whitespace-separated list of Unicode codepoints or
  63. ranges as hex numbers, optionally prefixed with 'U+', 'u', etc.
  64. For example, --unicodes=41-5a,61-7a adds ASCII letters, so does
  65. the more verbose --unicodes=U+0041-005A,U+0061-007A.
  66. The special strings '*' will choose all Unicode characters mapped
  67. by the font.
  68. --unicodes-file=<path>
  69. Like --unicodes, but reads from a file. Anything after a '#' on any
  70. line in the file is ignored as comments.
  71. --ignore-missing-glyphs
  72. Do not fail if some requested glyphs or gids are not available in
  73. the font.
  74. --no-ignore-missing-glyphs
  75. Stop and fail if some requested glyphs or gids are not available
  76. in the font. [default]
  77. --ignore-missing-unicodes [default]
  78. Do not fail if some requested Unicode characters (including those
  79. indirectly specified using --text or --text-file) are not available
  80. in the font.
  81. --no-ignore-missing-unicodes
  82. Stop and fail if some requested Unicode characters are not available
  83. in the font.
  84. Note the default discrepancy between ignoring missing glyphs versus
  85. unicodes. This is for historical reasons and in the future
  86. --no-ignore-missing-unicodes might become default.
  87. Other options:
  88. For the other options listed below, to see the current value of the option,
  89. pass a value of '?' to it, with or without a '='.
  90. Examples:
  91. $ pyftsubset --glyph-names?
  92. Current setting for 'glyph-names' is: False
  93. $ ./pyftsubset --name-IDs=?
  94. Current setting for 'name-IDs' is: [1, 2]
  95. $ ./pyftsubset --hinting? --no-hinting --hinting?
  96. Current setting for 'hinting' is: True
  97. Current setting for 'hinting' is: False
  98. Output options:
  99. --output-file=<path>
  100. The output font file. If not specified, the subsetted font
  101. will be saved in as font-file.subset.
  102. --flavor=<type>
  103. Specify flavor of output font file. May be 'woff' or 'woff2'.
  104. Note that WOFF2 requires the Brotli Python extension, available
  105. at https://github.com/google/brotli
  106. Glyph set expansion:
  107. These options control how additional glyphs are added to the subset.
  108. --notdef-glyph
  109. Add the '.notdef' glyph to the subset (ie, keep it). [default]
  110. --no-notdef-glyph
  111. Drop the '.notdef' glyph unless specified in the glyph set. This
  112. saves a few bytes, but is not possible for Postscript-flavored
  113. fonts, as those require '.notdef'. For TrueType-flavored fonts,
  114. this works fine as long as no unsupported glyphs are requested
  115. from the font.
  116. --notdef-outline
  117. Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is
  118. used when glyphs not supported by the font are to be shown. It is not
  119. needed otherwise.
  120. --no-notdef-outline
  121. When including a '.notdef' glyph, remove its outline. This saves
  122. a few bytes. [default]
  123. --recommended-glyphs
  124. Add glyphs 0, 1, 2, and 3 to the subset, as recommended for
  125. TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'.
  126. Some legacy software might require this, but no modern system does.
  127. --no-recommended-glyphs
  128. Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
  129. glyph set. [default]
  130. --layout-features[+|-]=<feature>[,<feature>...]
  131. Specify (=), add to (+=) or exclude from (-=) the comma-separated
  132. set of OpenType layout feature tags that will be preserved.
  133. Glyph variants used by the preserved features are added to the
  134. specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs',
  135. 'kern', 'liga', 'locl', 'mark', 'mkmk', 'rclt', 'rlig' and all features
  136. required for script shaping are preserved. To see the full list, try
  137. '--layout-features=?'. Use '*' to keep all features.
  138. Multiple --layout-features options can be provided if necessary.
  139. Examples:
  140. --layout-features+=onum,pnum,ss01
  141. * Keep the default set of features and 'onum', 'pnum', 'ss01'.
  142. --layout-features-='mark','mkmk'
  143. * Keep the default set of features but drop 'mark' and 'mkmk'.
  144. --layout-features='kern'
  145. * Only keep the 'kern' feature, drop all others.
  146. --layout-features=''
  147. * Drop all features.
  148. --layout-features='*'
  149. * Keep all features.
  150. --layout-features+=aalt --layout-features-=vrt2
  151. * Keep default set of features plus 'aalt', but drop 'vrt2'.
  152. Hinting options:
  153. --hinting
  154. Keep hinting [default]
  155. --no-hinting
  156. Drop glyph-specific hinting and font-wide hinting tables, as well
  157. as remove hinting-related bits and pieces from other tables (eg. GPOS).
  158. See --hinting-tables for list of tables that are dropped by default.
  159. Instructions and hints are stripped from 'glyf' and 'CFF ' tables
  160. respectively. This produces (sometimes up to 30%) smaller fonts that
  161. are suitable for extremely high-resolution systems, like high-end
  162. mobile devices and retina displays.
  163. XXX Note: Currently there is a known bug in 'CFF ' hint stripping that
  164. might make the font unusable as a webfont as they will be rejected by
  165. OpenType Sanitizer used in common browsers. For more information see:
  166. https://github.com/behdad/fonttools/issues/144
  167. The --desubroutinize options works around that bug.
  168. Optimization options:
  169. --desubroutinize
  170. Remove CFF use of subroutinizes. Subroutinization is a way to make CFF
  171. fonts smaller. For small subsets however, desubroutinizing might make
  172. the font smaller. It has even been reported that desubroutinized CFF
  173. fonts compress better (produce smaller output) WOFF and WOFF2 fonts.
  174. Also see note under --no-hinting.
  175. --no-desubroutinize [default]
  176. Leave CFF subroutinizes as is, only throw away unused subroutinizes.
  177. Font table options:
  178. --drop-tables[+|-]=<table>[,<table>...]
  179. Specify (=), add to (+=) or exclude from (-=) the comma-separated
  180. set of tables that will be be dropped.
  181. By default, the following tables are dropped:
  182. 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', 'PCLT', 'LTSH'
  183. and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'
  184. and color tables: 'CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'.
  185. The tool will attempt to subset the remaining tables.
  186. Examples:
  187. --drop-tables-='SVG '
  188. * Drop the default set of tables but keep 'SVG '.
  189. --drop-tables+=GSUB
  190. * Drop the default set of tables and 'GSUB'.
  191. --drop-tables=DSIG
  192. * Only drop the 'DSIG' table, keep all others.
  193. --drop-tables=
  194. * Keep all tables.
  195. --no-subset-tables+=<table>[,<table>...]
  196. Add to the set of tables that will not be subsetted.
  197. By default, the following tables are included in this list, as
  198. they do not need subsetting (ignore the fact that 'loca' is listed
  199. here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca',
  200. 'name', 'cvt ', 'fpgm', 'prep', 'VMDX', and 'DSIG'. Tables that the tool
  201. does not know how to subset and are not specified here will be dropped
  202. from the font.
  203. Example:
  204. --no-subset-tables+=FFTM
  205. * Keep 'FFTM' table in the font by preventing subsetting.
  206. --hinting-tables[-]=<table>[,<table>...]
  207. Specify (=), add to (+=) or exclude from (-=) the list of font-wide
  208. hinting tables that will be dropped if --no-hinting is specified,
  209. Examples:
  210. --hinting-tables-='VDMX'
  211. * Drop font-wide hinting tables except 'VDMX'.
  212. --hinting-tables=''
  213. * Keep all font-wide hinting tables (but strip hints from glyphs).
  214. --legacy-kern
  215. Keep TrueType 'kern' table even when OpenType 'GPOS' is available.
  216. --no-legacy-kern
  217. Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default]
  218. Font naming options:
  219. These options control what is retained in the 'name' table. For numerical
  220. codes, see: http://www.microsoft.com/typography/otspec/name.htm
  221. --name-IDs[+|-]=<nameID>[,<nameID>...]
  222. Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
  223. entry nameIDs that will be preserved. By default only nameID 1 (Family)
  224. and nameID 2 (Style) are preserved. Use '*' to keep all entries.
  225. Examples:
  226. --name-IDs+=0,4,6
  227. * Also keep Copyright, Full name and PostScript name entry.
  228. --name-IDs=''
  229. * Drop all 'name' table entries.
  230. --name-IDs='*'
  231. * keep all 'name' table entries
  232. --name-legacy
  233. Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.).
  234. XXX Note: This might be needed for some fonts that have no Unicode name
  235. entires for English. See: https://github.com/behdad/fonttools/issues/146
  236. --no-name-legacy
  237. Drop legacy (non-Unicode) 'name' table entries [default]
  238. --name-languages[+|-]=<langID>[,<langID>]
  239. Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
  240. langIDs that will be preserved. By default only records with langID
  241. 0x0409 (English) are preserved. Use '*' to keep all langIDs.
  242. --obfuscate-names
  243. Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4,
  244. and 6 with dummy strings (it is still fully functional as webfont).
  245. Glyph naming and encoding options:
  246. --glyph-names
  247. Keep PS glyph names in TT-flavored fonts. In general glyph names are
  248. not needed for correct use of the font. However, some PDF generators
  249. and PDF viewers might rely on glyph names to extract Unicode text
  250. from PDF documents.
  251. --no-glyph-names
  252. Drop PS glyph names in TT-flavored fonts, by using 'post' table
  253. version 3.0. [default]
  254. --legacy-cmap
  255. Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.).
  256. --no-legacy-cmap
  257. Drop the legacy 'cmap' subtables. [default]
  258. --symbol-cmap
  259. Keep the 3.0 symbol 'cmap'.
  260. --no-symbol-cmap
  261. Drop the 3.0 symbol 'cmap'. [default]
  262. Other font-specific options:
  263. --recalc-bounds
  264. Recalculate font bounding boxes.
  265. --no-recalc-bounds
  266. Keep original font bounding boxes. This is faster and still safe
  267. for all practical purposes. [default]
  268. --recalc-timestamp
  269. Set font 'modified' timestamp to current time.
  270. --no-recalc-timestamp
  271. Do not modify font 'modified' timestamp. [default]
  272. --canonical-order
  273. Order tables as recommended in the OpenType standard. This is not
  274. required by the standard, nor by any known implementation.
  275. --no-canonical-order
  276. Keep original order of font tables. This is faster. [default]
  277. Application options:
  278. --verbose
  279. Display verbose information of the subsetting process.
  280. --timing
  281. Display detailed timing information of the subsetting process.
  282. --xml
  283. Display the TTX XML representation of subsetted font.
  284. Example:
  285. Produce a subset containing the characters ' !"#$%' without performing
  286. size-reducing optimizations:
  287. $ pyftsubset font.ttf --unicodes="U+0020-0025" \\
  288. --layout-features='*' --glyph-names --symbol-cmap --legacy-cmap \\
  289. --notdef-glyph --notdef-outline --recommended-glyphs \\
  290. --name-IDs='*' --name-legacy --name-languages='*'
  291. """
  292. def _add_method(*clazzes):
  293. """Returns a decorator function that adds a new method to one or
  294. more classes."""
  295. def wrapper(method):
  296. for clazz in clazzes:
  297. assert clazz.__name__ != 'DefaultTable', \
  298. 'Oops, table class not found.'
  299. assert not hasattr(clazz, method.__name__), \
  300. "Oops, class '%s' has method '%s'." % (clazz.__name__,
  301. method.__name__)
  302. setattr(clazz, method.__name__, method)
  303. return None
  304. return wrapper
  305. def _uniq_sort(l):
  306. return sorted(set(l))
  307. def _set_update(s, *others):
  308. # Jython's set.update only takes one other argument.
  309. # Emulate real set.update...
  310. for other in others:
  311. s.update(other)
  312. def _dict_subset(d, glyphs):
  313. return {g:d[g] for g in glyphs}
  314. @_add_method(otTables.Coverage)
  315. def intersect(self, glyphs):
  316. """Returns ascending list of matching coverage values."""
  317. return [i for i,g in enumerate(self.glyphs) if g in glyphs]
  318. @_add_method(otTables.Coverage)
  319. def intersect_glyphs(self, glyphs):
  320. """Returns set of intersecting glyphs."""
  321. return set(g for g in self.glyphs if g in glyphs)
  322. @_add_method(otTables.Coverage)
  323. def subset(self, glyphs):
  324. """Returns ascending list of remaining coverage values."""
  325. indices = self.intersect(glyphs)
  326. self.glyphs = [g for g in self.glyphs if g in glyphs]
  327. return indices
  328. @_add_method(otTables.Coverage)
  329. def remap(self, coverage_map):
  330. """Remaps coverage."""
  331. self.glyphs = [self.glyphs[i] for i in coverage_map]
  332. @_add_method(otTables.ClassDef)
  333. def intersect(self, glyphs):
  334. """Returns ascending list of matching class values."""
  335. return _uniq_sort(
  336. ([0] if any(g not in self.classDefs for g in glyphs) else []) +
  337. [v for g,v in self.classDefs.items() if g in glyphs])
  338. @_add_method(otTables.ClassDef)
  339. def intersect_class(self, glyphs, klass):
  340. """Returns set of glyphs matching class."""
  341. if klass == 0:
  342. return set(g for g in glyphs if g not in self.classDefs)
  343. return set(g for g,v in self.classDefs.items()
  344. if v == klass and g in glyphs)
  345. @_add_method(otTables.ClassDef)
  346. def subset(self, glyphs, remap=False):
  347. """Returns ascending list of remaining classes."""
  348. self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs}
  349. # Note: while class 0 has the special meaning of "not matched",
  350. # if no glyph will ever /not match/, we can optimize class 0 out too.
  351. indices = _uniq_sort(
  352. ([0] if any(g not in self.classDefs for g in glyphs) else []) +
  353. list(self.classDefs.values()))
  354. if remap:
  355. self.remap(indices)
  356. return indices
  357. @_add_method(otTables.ClassDef)
  358. def remap(self, class_map):
  359. """Remaps classes."""
  360. self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()}
  361. @_add_method(otTables.SingleSubst)
  362. def closure_glyphs(self, s, cur_glyphs):
  363. s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs)
  364. @_add_method(otTables.SingleSubst)
  365. def subset_glyphs(self, s):
  366. self.mapping = {g:v for g,v in self.mapping.items()
  367. if g in s.glyphs and v in s.glyphs}
  368. return bool(self.mapping)
  369. @_add_method(otTables.MultipleSubst)
  370. def closure_glyphs(self, s, cur_glyphs):
  371. indices = self.Coverage.intersect(cur_glyphs)
  372. _set_update(s.glyphs, *(self.Sequence[i].Substitute for i in indices))
  373. @_add_method(otTables.MultipleSubst)
  374. def subset_glyphs(self, s):
  375. indices = self.Coverage.subset(s.glyphs)
  376. self.Sequence = [self.Sequence[i] for i in indices]
  377. # Now drop rules generating glyphs we don't want
  378. indices = [i for i,seq in enumerate(self.Sequence)
  379. if all(sub in s.glyphs for sub in seq.Substitute)]
  380. self.Sequence = [self.Sequence[i] for i in indices]
  381. self.Coverage.remap(indices)
  382. self.SequenceCount = len(self.Sequence)
  383. return bool(self.SequenceCount)
  384. @_add_method(otTables.AlternateSubst)
  385. def closure_glyphs(self, s, cur_glyphs):
  386. _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items()
  387. if g in cur_glyphs))
  388. @_add_method(otTables.AlternateSubst)
  389. def subset_glyphs(self, s):
  390. self.alternates = {g:vlist
  391. for g,vlist in self.alternates.items()
  392. if g in s.glyphs and
  393. all(v in s.glyphs for v in vlist)}
  394. return bool(self.alternates)
  395. @_add_method(otTables.LigatureSubst)
  396. def closure_glyphs(self, s, cur_glyphs):
  397. _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs
  398. if all(c in s.glyphs for c in seq.Component)]
  399. for g,seqs in self.ligatures.items()
  400. if g in cur_glyphs))
  401. @_add_method(otTables.LigatureSubst)
  402. def subset_glyphs(self, s):
  403. self.ligatures = {g:v for g,v in self.ligatures.items()
  404. if g in s.glyphs}
  405. self.ligatures = {g:[seq for seq in seqs
  406. if seq.LigGlyph in s.glyphs and
  407. all(c in s.glyphs for c in seq.Component)]
  408. for g,seqs in self.ligatures.items()}
  409. self.ligatures = {g:v for g,v in self.ligatures.items() if v}
  410. return bool(self.ligatures)
  411. @_add_method(otTables.ReverseChainSingleSubst)
  412. def closure_glyphs(self, s, cur_glyphs):
  413. if self.Format == 1:
  414. indices = self.Coverage.intersect(cur_glyphs)
  415. if(not indices or
  416. not all(c.intersect(s.glyphs)
  417. for c in self.LookAheadCoverage + self.BacktrackCoverage)):
  418. return
  419. s.glyphs.update(self.Substitute[i] for i in indices)
  420. else:
  421. assert 0, "unknown format: %s" % self.Format
  422. @_add_method(otTables.ReverseChainSingleSubst)
  423. def subset_glyphs(self, s):
  424. if self.Format == 1:
  425. indices = self.Coverage.subset(s.glyphs)
  426. self.Substitute = [self.Substitute[i] for i in indices]
  427. # Now drop rules generating glyphs we don't want
  428. indices = [i for i,sub in enumerate(self.Substitute)
  429. if sub in s.glyphs]
  430. self.Substitute = [self.Substitute[i] for i in indices]
  431. self.Coverage.remap(indices)
  432. self.GlyphCount = len(self.Substitute)
  433. return bool(self.GlyphCount and
  434. all(c.subset(s.glyphs)
  435. for c in self.LookAheadCoverage+self.BacktrackCoverage))
  436. else:
  437. assert 0, "unknown format: %s" % self.Format
  438. @_add_method(otTables.SinglePos)
  439. def subset_glyphs(self, s):
  440. if self.Format == 1:
  441. return len(self.Coverage.subset(s.glyphs))
  442. elif self.Format == 2:
  443. indices = self.Coverage.subset(s.glyphs)
  444. self.Value = [self.Value[i] for i in indices]
  445. self.ValueCount = len(self.Value)
  446. return bool(self.ValueCount)
  447. else:
  448. assert 0, "unknown format: %s" % self.Format
  449. @_add_method(otTables.SinglePos)
  450. def prune_post_subset(self, options):
  451. if not options.hinting:
  452. # Drop device tables
  453. self.ValueFormat &= ~0x00F0
  454. return True
  455. @_add_method(otTables.PairPos)
  456. def subset_glyphs(self, s):
  457. if self.Format == 1:
  458. indices = self.Coverage.subset(s.glyphs)
  459. self.PairSet = [self.PairSet[i] for i in indices]
  460. for p in self.PairSet:
  461. p.PairValueRecord = [r for r in p.PairValueRecord
  462. if r.SecondGlyph in s.glyphs]
  463. p.PairValueCount = len(p.PairValueRecord)
  464. # Remove empty pairsets
  465. indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount]
  466. self.Coverage.remap(indices)
  467. self.PairSet = [self.PairSet[i] for i in indices]
  468. self.PairSetCount = len(self.PairSet)
  469. return bool(self.PairSetCount)
  470. elif self.Format == 2:
  471. class1_map = self.ClassDef1.subset(s.glyphs, remap=True)
  472. class2_map = self.ClassDef2.subset(s.glyphs, remap=True)
  473. self.Class1Record = [self.Class1Record[i] for i in class1_map]
  474. for c in self.Class1Record:
  475. c.Class2Record = [c.Class2Record[i] for i in class2_map]
  476. self.Class1Count = len(class1_map)
  477. self.Class2Count = len(class2_map)
  478. return bool(self.Class1Count and
  479. self.Class2Count and
  480. self.Coverage.subset(s.glyphs))
  481. else:
  482. assert 0, "unknown format: %s" % self.Format
  483. @_add_method(otTables.PairPos)
  484. def prune_post_subset(self, options):
  485. if not options.hinting:
  486. # Drop device tables
  487. self.ValueFormat1 &= ~0x00F0
  488. self.ValueFormat2 &= ~0x00F0
  489. return True
  490. @_add_method(otTables.CursivePos)
  491. def subset_glyphs(self, s):
  492. if self.Format == 1:
  493. indices = self.Coverage.subset(s.glyphs)
  494. self.EntryExitRecord = [self.EntryExitRecord[i] for i in indices]
  495. self.EntryExitCount = len(self.EntryExitRecord)
  496. return bool(self.EntryExitCount)
  497. else:
  498. assert 0, "unknown format: %s" % self.Format
  499. @_add_method(otTables.Anchor)
  500. def prune_hints(self):
  501. # Drop device tables / contour anchor point
  502. self.ensureDecompiled()
  503. self.Format = 1
  504. @_add_method(otTables.CursivePos)
  505. def prune_post_subset(self, options):
  506. if not options.hinting:
  507. for rec in self.EntryExitRecord:
  508. if rec.EntryAnchor: rec.EntryAnchor.prune_hints()
  509. if rec.ExitAnchor: rec.ExitAnchor.prune_hints()
  510. return True
  511. @_add_method(otTables.MarkBasePos)
  512. def subset_glyphs(self, s):
  513. if self.Format == 1:
  514. mark_indices = self.MarkCoverage.subset(s.glyphs)
  515. self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
  516. for i in mark_indices]
  517. self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
  518. base_indices = self.BaseCoverage.subset(s.glyphs)
  519. self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i]
  520. for i in base_indices]
  521. self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
  522. # Prune empty classes
  523. class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
  524. self.ClassCount = len(class_indices)
  525. for m in self.MarkArray.MarkRecord:
  526. m.Class = class_indices.index(m.Class)
  527. for b in self.BaseArray.BaseRecord:
  528. b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices]
  529. return bool(self.ClassCount and
  530. self.MarkArray.MarkCount and
  531. self.BaseArray.BaseCount)
  532. else:
  533. assert 0, "unknown format: %s" % self.Format
  534. @_add_method(otTables.MarkBasePos)
  535. def prune_post_subset(self, options):
  536. if not options.hinting:
  537. for m in self.MarkArray.MarkRecord:
  538. if m.MarkAnchor:
  539. m.MarkAnchor.prune_hints()
  540. for b in self.BaseArray.BaseRecord:
  541. for a in b.BaseAnchor:
  542. if a:
  543. a.prune_hints()
  544. return True
  545. @_add_method(otTables.MarkLigPos)
  546. def subset_glyphs(self, s):
  547. if self.Format == 1:
  548. mark_indices = self.MarkCoverage.subset(s.glyphs)
  549. self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
  550. for i in mark_indices]
  551. self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
  552. ligature_indices = self.LigatureCoverage.subset(s.glyphs)
  553. self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i]
  554. for i in ligature_indices]
  555. self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
  556. # Prune empty classes
  557. class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
  558. self.ClassCount = len(class_indices)
  559. for m in self.MarkArray.MarkRecord:
  560. m.Class = class_indices.index(m.Class)
  561. for l in self.LigatureArray.LigatureAttach:
  562. for c in l.ComponentRecord:
  563. c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices]
  564. return bool(self.ClassCount and
  565. self.MarkArray.MarkCount and
  566. self.LigatureArray.LigatureCount)
  567. else:
  568. assert 0, "unknown format: %s" % self.Format
  569. @_add_method(otTables.MarkLigPos)
  570. def prune_post_subset(self, options):
  571. if not options.hinting:
  572. for m in self.MarkArray.MarkRecord:
  573. if m.MarkAnchor:
  574. m.MarkAnchor.prune_hints()
  575. for l in self.LigatureArray.LigatureAttach:
  576. for c in l.ComponentRecord:
  577. for a in c.LigatureAnchor:
  578. if a:
  579. a.prune_hints()
  580. return True
  581. @_add_method(otTables.MarkMarkPos)
  582. def subset_glyphs(self, s):
  583. if self.Format == 1:
  584. mark1_indices = self.Mark1Coverage.subset(s.glyphs)
  585. self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i]
  586. for i in mark1_indices]
  587. self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
  588. mark2_indices = self.Mark2Coverage.subset(s.glyphs)
  589. self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i]
  590. for i in mark2_indices]
  591. self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
  592. # Prune empty classes
  593. class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
  594. self.ClassCount = len(class_indices)
  595. for m in self.Mark1Array.MarkRecord:
  596. m.Class = class_indices.index(m.Class)
  597. for b in self.Mark2Array.Mark2Record:
  598. b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices]
  599. return bool(self.ClassCount and
  600. self.Mark1Array.MarkCount and
  601. self.Mark2Array.MarkCount)
  602. else:
  603. assert 0, "unknown format: %s" % self.Format
  604. @_add_method(otTables.MarkMarkPos)
  605. def prune_post_subset(self, options):
  606. if not options.hinting:
  607. # Drop device tables or contour anchor point
  608. for m in self.Mark1Array.MarkRecord:
  609. if m.MarkAnchor:
  610. m.MarkAnchor.prune_hints()
  611. for b in self.Mark2Array.Mark2Record:
  612. for m in b.Mark2Anchor:
  613. if m:
  614. m.prune_hints()
  615. return True
  616. @_add_method(otTables.SingleSubst,
  617. otTables.MultipleSubst,
  618. otTables.AlternateSubst,
  619. otTables.LigatureSubst,
  620. otTables.ReverseChainSingleSubst,
  621. otTables.SinglePos,
  622. otTables.PairPos,
  623. otTables.CursivePos,
  624. otTables.MarkBasePos,
  625. otTables.MarkLigPos,
  626. otTables.MarkMarkPos)
  627. def subset_lookups(self, lookup_indices):
  628. pass
  629. @_add_method(otTables.SingleSubst,
  630. otTables.MultipleSubst,
  631. otTables.AlternateSubst,
  632. otTables.LigatureSubst,
  633. otTables.ReverseChainSingleSubst,
  634. otTables.SinglePos,
  635. otTables.PairPos,
  636. otTables.CursivePos,
  637. otTables.MarkBasePos,
  638. otTables.MarkLigPos,
  639. otTables.MarkMarkPos)
  640. def collect_lookups(self):
  641. return []
  642. @_add_method(otTables.SingleSubst,
  643. otTables.MultipleSubst,
  644. otTables.AlternateSubst,
  645. otTables.LigatureSubst,
  646. otTables.ReverseChainSingleSubst,
  647. otTables.ContextSubst,
  648. otTables.ChainContextSubst,
  649. otTables.ContextPos,
  650. otTables.ChainContextPos)
  651. def prune_post_subset(self, options):
  652. return True
  653. @_add_method(otTables.SingleSubst,
  654. otTables.AlternateSubst,
  655. otTables.ReverseChainSingleSubst)
  656. def may_have_non_1to1(self):
  657. return False
  658. @_add_method(otTables.MultipleSubst,
  659. otTables.LigatureSubst,
  660. otTables.ContextSubst,
  661. otTables.ChainContextSubst)
  662. def may_have_non_1to1(self):
  663. return True
  664. @_add_method(otTables.ContextSubst,
  665. otTables.ChainContextSubst,
  666. otTables.ContextPos,
  667. otTables.ChainContextPos)
  668. def __subset_classify_context(self):
  669. class ContextHelper(object):
  670. def __init__(self, klass, Format):
  671. if klass.__name__.endswith('Subst'):
  672. Typ = 'Sub'
  673. Type = 'Subst'
  674. else:
  675. Typ = 'Pos'
  676. Type = 'Pos'
  677. if klass.__name__.startswith('Chain'):
  678. Chain = 'Chain'
  679. else:
  680. Chain = ''
  681. ChainTyp = Chain+Typ
  682. self.Typ = Typ
  683. self.Type = Type
  684. self.Chain = Chain
  685. self.ChainTyp = ChainTyp
  686. self.LookupRecord = Type+'LookupRecord'
  687. if Format == 1:
  688. Coverage = lambda r: r.Coverage
  689. ChainCoverage = lambda r: r.Coverage
  690. ContextData = lambda r:(None,)
  691. ChainContextData = lambda r:(None, None, None)
  692. RuleData = lambda r:(r.Input,)
  693. ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
  694. SetRuleData = None
  695. ChainSetRuleData = None
  696. elif Format == 2:
  697. Coverage = lambda r: r.Coverage
  698. ChainCoverage = lambda r: r.Coverage
  699. ContextData = lambda r:(r.ClassDef,)
  700. ChainContextData = lambda r:(r.BacktrackClassDef,
  701. r.InputClassDef,
  702. r.LookAheadClassDef)
  703. RuleData = lambda r:(r.Class,)
  704. ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
  705. def SetRuleData(r, d):(r.Class,) = d
  706. def ChainSetRuleData(r, d):(r.Backtrack, r.Input, r.LookAhead) = d
  707. elif Format == 3:
  708. Coverage = lambda r: r.Coverage[0]
  709. ChainCoverage = lambda r: r.InputCoverage[0]
  710. ContextData = None
  711. ChainContextData = None
  712. RuleData = lambda r: r.Coverage
  713. ChainRuleData = lambda r:(r.BacktrackCoverage +
  714. r.InputCoverage +
  715. r.LookAheadCoverage)
  716. SetRuleData = None
  717. ChainSetRuleData = None
  718. else:
  719. assert 0, "unknown format: %s" % Format
  720. if Chain:
  721. self.Coverage = ChainCoverage
  722. self.ContextData = ChainContextData
  723. self.RuleData = ChainRuleData
  724. self.SetRuleData = ChainSetRuleData
  725. else:
  726. self.Coverage = Coverage
  727. self.ContextData = ContextData
  728. self.RuleData = RuleData
  729. self.SetRuleData = SetRuleData
  730. if Format == 1:
  731. self.Rule = ChainTyp+'Rule'
  732. self.RuleCount = ChainTyp+'RuleCount'
  733. self.RuleSet = ChainTyp+'RuleSet'
  734. self.RuleSetCount = ChainTyp+'RuleSetCount'
  735. self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
  736. elif Format == 2:
  737. self.Rule = ChainTyp+'ClassRule'
  738. self.RuleCount = ChainTyp+'ClassRuleCount'
  739. self.RuleSet = ChainTyp+'ClassSet'
  740. self.RuleSetCount = ChainTyp+'ClassSetCount'
  741. self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c
  742. else (set(glyphs) if r == 0 else set()))
  743. self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
  744. self.ClassDefIndex = 1 if Chain else 0
  745. self.Input = 'Input' if Chain else 'Class'
  746. if self.Format not in [1, 2, 3]:
  747. return None # Don't shoot the messenger; let it go
  748. if not hasattr(self.__class__, "__ContextHelpers"):
  749. self.__class__.__ContextHelpers = {}
  750. if self.Format not in self.__class__.__ContextHelpers:
  751. helper = ContextHelper(self.__class__, self.Format)
  752. self.__class__.__ContextHelpers[self.Format] = helper
  753. return self.__class__.__ContextHelpers[self.Format]
  754. @_add_method(otTables.ContextSubst,
  755. otTables.ChainContextSubst)
  756. def closure_glyphs(self, s, cur_glyphs):
  757. c = self.__subset_classify_context()
  758. indices = c.Coverage(self).intersect(cur_glyphs)
  759. if not indices:
  760. return []
  761. cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs)
  762. if self.Format == 1:
  763. ContextData = c.ContextData(self)
  764. rss = getattr(self, c.RuleSet)
  765. rssCount = getattr(self, c.RuleSetCount)
  766. for i in indices:
  767. if i >= rssCount or not rss[i]: continue
  768. for r in getattr(rss[i], c.Rule):
  769. if not r: continue
  770. if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
  771. for cd,klist in zip(ContextData, c.RuleData(r))):
  772. continue
  773. chaos = set()
  774. for ll in getattr(r, c.LookupRecord):
  775. if not ll: continue
  776. seqi = ll.SequenceIndex
  777. if seqi in chaos:
  778. # TODO Can we improve this?
  779. pos_glyphs = None
  780. else:
  781. if seqi == 0:
  782. pos_glyphs = frozenset([c.Coverage(self).glyphs[i]])
  783. else:
  784. pos_glyphs = frozenset([r.Input[seqi - 1]])
  785. lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
  786. chaos.add(seqi)
  787. if lookup.may_have_non_1to1():
  788. chaos.update(range(seqi, len(r.Input)+2))
  789. lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
  790. elif self.Format == 2:
  791. ClassDef = getattr(self, c.ClassDef)
  792. indices = ClassDef.intersect(cur_glyphs)
  793. ContextData = c.ContextData(self)
  794. rss = getattr(self, c.RuleSet)
  795. rssCount = getattr(self, c.RuleSetCount)
  796. for i in indices:
  797. if i >= rssCount or not rss[i]: continue
  798. for r in getattr(rss[i], c.Rule):
  799. if not r: continue
  800. if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
  801. for cd,klist in zip(ContextData, c.RuleData(r))):
  802. continue
  803. chaos = set()
  804. for ll in getattr(r, c.LookupRecord):
  805. if not ll: continue
  806. seqi = ll.SequenceIndex
  807. if seqi in chaos:
  808. # TODO Can we improve this?
  809. pos_glyphs = None
  810. else:
  811. if seqi == 0:
  812. pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i))
  813. else:
  814. pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1]))
  815. lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
  816. chaos.add(seqi)
  817. if lookup.may_have_non_1to1():
  818. chaos.update(range(seqi, len(getattr(r, c.Input))+2))
  819. lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
  820. elif self.Format == 3:
  821. if not all(x.intersect(s.glyphs) for x in c.RuleData(self)):
  822. return []
  823. r = self
  824. chaos = set()
  825. for ll in getattr(r, c.LookupRecord):
  826. if not ll: continue
  827. seqi = ll.SequenceIndex
  828. if seqi in chaos:
  829. # TODO Can we improve this?
  830. pos_glyphs = None
  831. else:
  832. if seqi == 0:
  833. pos_glyphs = frozenset(cur_glyphs)
  834. else:
  835. pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs))
  836. lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
  837. chaos.add(seqi)
  838. if lookup.may_have_non_1to1():
  839. chaos.update(range(seqi, len(r.InputCoverage)+1))
  840. lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
  841. else:
  842. assert 0, "unknown format: %s" % self.Format
  843. @_add_method(otTables.ContextSubst,
  844. otTables.ContextPos,
  845. otTables.ChainContextSubst,
  846. otTables.ChainContextPos)
  847. def subset_glyphs(self, s):
  848. c = self.__subset_classify_context()
  849. if self.Format == 1:
  850. indices = self.Coverage.subset(s.glyphs)
  851. rss = getattr(self, c.RuleSet)
  852. rssCount = getattr(self, c.RuleSetCount)
  853. rss = [rss[i] for i in indices if i < rssCount]
  854. for rs in rss:
  855. if not rs: continue
  856. ss = getattr(rs, c.Rule)
  857. ss = [r for r in ss
  858. if r and all(all(g in s.glyphs for g in glist)
  859. for glist in c.RuleData(r))]
  860. setattr(rs, c.Rule, ss)
  861. setattr(rs, c.RuleCount, len(ss))
  862. # Prune empty rulesets
  863. indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)]
  864. self.Coverage.remap(indices)
  865. rss = [rss[i] for i in indices]
  866. setattr(self, c.RuleSet, rss)
  867. setattr(self, c.RuleSetCount, len(rss))
  868. return bool(rss)
  869. elif self.Format == 2:
  870. if not self.Coverage.subset(s.glyphs):
  871. return False
  872. ContextData = c.ContextData(self)
  873. klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData]
  874. # Keep rulesets for class numbers that survived.
  875. indices = klass_maps[c.ClassDefIndex]
  876. rss = getattr(self, c.RuleSet)
  877. rssCount = getattr(self, c.RuleSetCount)
  878. rss = [rss[i] for i in indices if i < rssCount]
  879. del rssCount
  880. # Delete, but not renumber, unreachable rulesets.
  881. indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs)
  882. rss = [rss if i in indices else None for i,rss in enumerate(rss)]
  883. for rs in rss:
  884. if not rs: continue
  885. ss = getattr(rs, c.Rule)
  886. ss = [r for r in ss
  887. if r and all(all(k in klass_map for k in klist)
  888. for klass_map,klist in zip(klass_maps, c.RuleData(r)))]
  889. setattr(rs, c.Rule, ss)
  890. setattr(rs, c.RuleCount, len(ss))
  891. # Remap rule classes
  892. for r in ss:
  893. c.SetRuleData(r, [[klass_map.index(k) for k in klist]
  894. for klass_map,klist in zip(klass_maps, c.RuleData(r))])
  895. # Prune empty rulesets
  896. rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss]
  897. while rss and rss[-1] is None:
  898. del rss[-1]
  899. setattr(self, c.RuleSet, rss)
  900. setattr(self, c.RuleSetCount, len(rss))
  901. # TODO: We can do a second round of remapping class values based
  902. # on classes that are actually used in at least one rule. Right
  903. # now we subset classes to c.glyphs only. Or better, rewrite
  904. # the above to do that.
  905. return bool(rss)
  906. elif self.Format == 3:
  907. return all(x.subset(s.glyphs) for x in c.RuleData(self))
  908. else:
  909. assert 0, "unknown format: %s" % self.Format
  910. @_add_method(otTables.ContextSubst,
  911. otTables.ChainContextSubst,
  912. otTables.ContextPos,
  913. otTables.ChainContextPos)
  914. def subset_lookups(self, lookup_indices):
  915. c = self.__subset_classify_context()
  916. if self.Format in [1, 2]:
  917. for rs in getattr(self, c.RuleSet):
  918. if not rs: continue
  919. for r in getattr(rs, c.Rule):
  920. if not r: continue
  921. setattr(r, c.LookupRecord,
  922. [ll for ll in getattr(r, c.LookupRecord)
  923. if ll and ll.LookupListIndex in lookup_indices])
  924. for ll in getattr(r, c.LookupRecord):
  925. if not ll: continue
  926. ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
  927. elif self.Format == 3:
  928. setattr(self, c.LookupRecord,
  929. [ll for ll in getattr(self, c.LookupRecord)
  930. if ll and ll.LookupListIndex in lookup_indices])
  931. for ll in getattr(self, c.LookupRecord):
  932. if not ll: continue
  933. ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
  934. else:
  935. assert 0, "unknown format: %s" % self.Format
  936. @_add_method(otTables.ContextSubst,
  937. otTables.ChainContextSubst,
  938. otTables.ContextPos,
  939. otTables.ChainContextPos)
  940. def collect_lookups(self):
  941. c = self.__subset_classify_context()
  942. if self.Format in [1, 2]:
  943. return [ll.LookupListIndex
  944. for rs in getattr(self, c.RuleSet) if rs
  945. for r in getattr(rs, c.Rule) if r
  946. for ll in getattr(r, c.LookupRecord) if ll]
  947. elif self.Format == 3:
  948. return [ll.LookupListIndex
  949. for ll in getattr(self, c.LookupRecord) if ll]
  950. else:
  951. assert 0, "unknown format: %s" % self.Format
  952. @_add_method(otTables.ExtensionSubst)
  953. def closure_glyphs(self, s, cur_glyphs):
  954. if self.Format == 1:
  955. self.ExtSubTable.closure_glyphs(s, cur_glyphs)
  956. else:
  957. assert 0, "unknown format: %s" % self.Format
  958. @_add_method(otTables.ExtensionSubst)
  959. def may_have_non_1to1(self):
  960. if self.Format == 1:
  961. return self.ExtSubTable.may_have_non_1to1()
  962. else:
  963. assert 0, "unknown format: %s" % self.Format
  964. @_add_method(otTables.ExtensionSubst,
  965. otTables.ExtensionPos)
  966. def subset_glyphs(self, s):
  967. if self.Format == 1:
  968. return self.ExtSubTable.subset_glyphs(s)
  969. else:
  970. assert 0, "unknown format: %s" % self.Format
  971. @_add_method(otTables.ExtensionSubst,
  972. otTables.ExtensionPos)
  973. def prune_post_subset(self, options):
  974. if self.Format == 1:
  975. return self.ExtSubTable.prune_post_subset(options)
  976. else:
  977. assert 0, "unknown format: %s" % self.Format
  978. @_add_method(otTables.ExtensionSubst,
  979. otTables.ExtensionPos)
  980. def subset_lookups(self, lookup_indices):
  981. if self.Format == 1:
  982. return self.ExtSubTable.subset_lookups(lookup_indices)
  983. else:
  984. assert 0, "unknown format: %s" % self.Format
  985. @_add_method(otTables.ExtensionSubst,
  986. otTables.ExtensionPos)
  987. def collect_lookups(self):
  988. if self.Format == 1:
  989. return self.ExtSubTable.collect_lookups()
  990. else:
  991. assert 0, "unknown format: %s" % self.Format
  992. @_add_method(otTables.Lookup)
  993. def closure_glyphs(self, s, cur_glyphs=None):
  994. if cur_glyphs is None:
  995. cur_glyphs = frozenset(s.glyphs)
  996. # Memoize
  997. if (id(self), cur_glyphs) in s._doneLookups:
  998. return
  999. s._doneLookups.add((id(self), cur_glyphs))
  1000. if self in s._activeLookups:
  1001. raise Exception("Circular loop in lookup recursion")
  1002. s._activeLookups.append(self)
  1003. for st in self.SubTable:
  1004. if not st: continue
  1005. st.closure_glyphs(s, cur_glyphs)
  1006. assert(s._activeLookups[-1] == self)
  1007. del s._activeLookups[-1]
  1008. @_add_method(otTables.Lookup)
  1009. def subset_glyphs(self, s):
  1010. self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)]
  1011. self.SubTableCount = len(self.SubTable)
  1012. return bool(self.SubTableCount)
  1013. @_add_method(otTables.Lookup)
  1014. def prune_post_subset(self, options):
  1015. ret = False
  1016. for st in self.SubTable:
  1017. if not st: continue
  1018. if st.prune_post_subset(options): ret = True
  1019. return ret
  1020. @_add_method(otTables.Lookup)
  1021. def subset_lookups(self, lookup_indices):
  1022. for s in self.SubTable:
  1023. s.subset_lookups(lookup_indices)
  1024. @_add_method(otTables.Lookup)
  1025. def collect_lookups(self):
  1026. return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable
  1027. if st), []))
  1028. @_add_method(otTables.Lookup)
  1029. def may_have_non_1to1(self):
  1030. return any(st.may_have_non_1to1() for st in self.SubTable if st)
  1031. @_add_method(otTables.LookupList)
  1032. def subset_glyphs(self, s):
  1033. """Returns the indices of nonempty lookups."""
  1034. return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)]
  1035. @_add_method(otTables.LookupList)
  1036. def prune_post_subset(self, options):
  1037. ret = False
  1038. for l in self.Lookup:
  1039. if not l: continue
  1040. if l.prune_post_subset(options): ret = True
  1041. return ret
  1042. @_add_method(otTables.LookupList)
  1043. def subset_lookups(self, lookup_indices):
  1044. self.ensureDecompiled()
  1045. self.Lookup = [self.Lookup[i] for i in lookup_indices
  1046. if i < self.LookupCount]
  1047. self.LookupCount = len(self.Lookup)
  1048. for l in self.Lookup:
  1049. l.subset_lookups(lookup_indices)
  1050. @_add_method(otTables.LookupList)
  1051. def neuter_lookups(self, lookup_indices):
  1052. """Sets lookups not in lookup_indices to None."""
  1053. self.ensureDecompiled()
  1054. self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)]
  1055. @_add_method(otTables.LookupList)
  1056. def closure_lookups(self, lookup_indices):
  1057. lookup_indices = _uniq_sort(lookup_indices)
  1058. recurse = lookup_indices
  1059. while True:
  1060. recurse_lookups = sum((self.Lookup[i].collect_lookups()
  1061. for i in recurse if i < self.LookupCount), [])
  1062. recurse_lookups = [l for l in recurse_lookups
  1063. if l not in lookup_indices and l < self.LookupCount]
  1064. if not recurse_lookups:
  1065. return _uniq_sort(lookup_indices)
  1066. recurse_lookups = _uniq_sort(recurse_lookups)
  1067. lookup_indices.extend(recurse_lookups)
  1068. recurse = recurse_lookups
  1069. @_add_method(otTables.Feature)
  1070. def subset_lookups(self, lookup_indices):
  1071. self.LookupListIndex = [l for l in self.LookupListIndex
  1072. if l in lookup_indices]
  1073. # Now map them.
  1074. self.LookupListIndex = [lookup_indices.index(l)
  1075. for l in self.LookupListIndex]
  1076. self.LookupCount = len(self.LookupListIndex)
  1077. return self.LookupCount or self.FeatureParams
  1078. @_add_method(otTables.Feature)
  1079. def collect_lookups(self):
  1080. return self.LookupListIndex[:]
  1081. @_add_method(otTables.FeatureList)
  1082. def subset_lookups(self, lookup_indices):
  1083. """Returns the indices of nonempty features."""
  1084. # Note: Never ever drop feature 'pref', even if it's empty.
  1085. # HarfBuzz chooses shaper for Khmer based on presence of this
  1086. # feature. See thread at:
  1087. # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html
  1088. feature_indices = [i for i,f in enumerate(self.FeatureRecord)
  1089. if (f.Feature.subset_lookups(lookup_indices) or
  1090. f.FeatureTag == 'pref')]
  1091. self.subset_features(feature_indices)
  1092. return feature_indices
  1093. @_add_method(otTables.FeatureList)
  1094. def collect_lookups(self, feature_indices):
  1095. return _uniq_sort(sum((self.FeatureRecord[i].Feature.collect_lookups()
  1096. for i in feature_indices
  1097. if i < self.FeatureCount), []))
  1098. @_add_method(otTables.FeatureList)
  1099. def subset_features(self, feature_indices):
  1100. self.ensureDecompiled()
  1101. self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices]
  1102. self.FeatureCount = len(self.FeatureRecord)
  1103. return bool(self.FeatureCount)
  1104. @_add_method(otTables.DefaultLangSys,
  1105. otTables.LangSys)
  1106. def subset_features(self, feature_indices):
  1107. if self.ReqFeatureIndex in feature_indices:
  1108. self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
  1109. else:
  1110. self.ReqFeatureIndex = 65535
  1111. self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
  1112. # Now map them.
  1113. self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex
  1114. if f in feature_indices]
  1115. self.FeatureCount = len(self.FeatureIndex)
  1116. return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
  1117. @_add_method(otTables.DefaultLangSys,
  1118. otTables.LangSys)
  1119. def collect_features(self):
  1120. feature_indices = self.FeatureIndex[:]
  1121. if self.ReqFeatureIndex != 65535:
  1122. feature_indices.append(self.ReqFeatureIndex)
  1123. return _uniq_sort(feature_indices)
  1124. @_add_method(otTables.Script)
  1125. def subset_features(self, feature_indices):
  1126. if(self.DefaultLangSys and
  1127. not self.DefaultLangSys.subset_features(feature_indices)):
  1128. self.DefaultLangSys = None
  1129. self.LangSysRecord = [l for l in self.LangSysRecord
  1130. if l.LangSys.subset_features(feature_indices)]
  1131. self.LangSysCount = len(self.LangSysRecord)
  1132. return bool(self.LangSysCount or self.DefaultLangSys)
  1133. @_add_method(otTables.Script)
  1134. def collect_features(self):
  1135. feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
  1136. if self.DefaultLangSys:
  1137. feature_indices.append(self.DefaultLangSys.collect_features())
  1138. return _uniq_sort(sum(feature_indices, []))
  1139. @_add_method(otTables.ScriptList)
  1140. def subset_features(self, feature_indices):
  1141. self.ScriptRecord = [s for s in self.ScriptRecord
  1142. if s.Script.subset_features(feature_indices)]
  1143. self.ScriptCount = len(self.ScriptRecord)
  1144. return bool(self.ScriptCount)
  1145. @_add_method(otTables.ScriptList)
  1146. def collect_features(self):
  1147. return _uniq_sort(sum((s.Script.collect_features()
  1148. for s in self.ScriptRecord), []))
  1149. @_add_method(ttLib.getTableClass('GSUB'))
  1150. def closure_glyphs(self, s):
  1151. s.table = self.table
  1152. if self.table.ScriptList:
  1153. feature_indices = self.table.ScriptList.collect_features()
  1154. else:
  1155. feature_indices = []
  1156. if self.table.FeatureList:
  1157. lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
  1158. else:
  1159. lookup_indices = []
  1160. if self.table.LookupList:
  1161. while True:
  1162. orig_glyphs = frozenset(s.glyphs)
  1163. s._activeLookups = []
  1164. s._doneLookups = set()
  1165. for i in lookup_indices:
  1166. if i >= self.table.LookupList.LookupCount: continue
  1167. if not self.table.LookupList.Lookup[i]: continue
  1168. self.table.LookupList.Lookup[i].closure_glyphs(s)
  1169. del s._activeLookups, s._doneLookups
  1170. if orig_glyphs == s.glyphs:
  1171. break
  1172. del s.table
  1173. @_add_method(ttLib.getTableClass('GSUB'),
  1174. ttLib.getTableClass('GPOS'))
  1175. def subset_glyphs(self, s):
  1176. s.glyphs = s.glyphs_gsubed
  1177. if self.table.LookupList:
  1178. lookup_indices = self.table.LookupList.subset_glyphs(s)
  1179. else:
  1180. lookup_indices = []
  1181. self.subset_lookups(lookup_indices)
  1182. return True
  1183. @_add_method(ttLib.getTableClass('GSUB'),
  1184. ttLib.getTableClass('GPOS'))
  1185. def subset_lookups(self, lookup_indices):
  1186. """Retains specified lookups, then removes empty features, language
  1187. systems, and scripts."""
  1188. if self.table.LookupList:
  1189. self.table.LookupList.subset_lookups(lookup_indices)
  1190. if self.table.FeatureList:
  1191. feature_indices = self.table.FeatureList.subset_lookups(lookup_indices)
  1192. else:
  1193. feature_indices = []
  1194. if self.table.ScriptList:
  1195. self.table.ScriptList.subset_features(feature_indices)
  1196. @_add_method(ttLib.getTableClass('GSUB'),
  1197. ttLib.getTableClass('GPOS'))
  1198. def neuter_lookups(self, lookup_indices):
  1199. """Sets lookups not in lookup_indices to None."""
  1200. if self.table.LookupList:
  1201. self.table.LookupList.neuter_lookups(lookup_indices)
  1202. @_add_method(ttLib.getTableClass('GSUB'),
  1203. ttLib.getTableClass('GPOS'))
  1204. def prune_lookups(self, remap=True):
  1205. """Remove (default) or neuter unreferenced lookups"""
  1206. if self.table.ScriptList:
  1207. feature_indices = self.table.ScriptList.collect_features()
  1208. else:
  1209. feature_indices = []
  1210. if self.table.FeatureList:
  1211. lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
  1212. else:
  1213. lookup_indices = []
  1214. if self.table.LookupList:
  1215. lookup_indices = self.table.LookupList.closure_lookups(lookup_indices)
  1216. else:
  1217. lookup_indices = []
  1218. if remap:
  1219. self.subset_lookups(lookup_indices)
  1220. else:
  1221. self.neuter_lookups(lookup_indices)
  1222. @_add_method(ttLib.getTableClass('GSUB'),
  1223. ttLib.getTableClass('GPOS'))
  1224. def subset_feature_tags(self, feature_tags):
  1225. if self.table.FeatureList:
  1226. feature_indices = \
  1227. [i for i,f in enumerate(self.table.FeatureList.FeatureRecord)
  1228. if f.FeatureTag in feature_tags]
  1229. self.table.FeatureList.subset_features(feature_indices)
  1230. else:
  1231. feature_indices = []
  1232. if self.table.ScriptList:
  1233. self.table.ScriptList.subset_features(feature_indices)
  1234. @_add_method(ttLib.getTableClass('GSUB'),
  1235. ttLib.getTableClass('GPOS'))
  1236. def prune_features(self):
  1237. """Remove unreferenced features"""
  1238. if self.table.ScriptList:
  1239. feature_indices = self.table.ScriptList.collect_features()
  1240. else:
  1241. feature_indices = []
  1242. if self.table.FeatureList:
  1243. self.table.FeatureList.subset_features(feature_indices)
  1244. if self.table.ScriptList:
  1245. self.table.ScriptList.subset_features(feature_indices)
  1246. @_add_method(ttLib.getTableClass('GSUB'),
  1247. ttLib.getTableClass('GPOS'))
  1248. def prune_pre_subset(self, options):
  1249. # Drop undesired features
  1250. if '*' not in options.layout_features:
  1251. self.subset_feature_tags(options.layout_features)
  1252. # Neuter unreferenced lookups
  1253. self.prune_lookups(remap=False)
  1254. return True
  1255. @_add_method(ttLib.getTableClass('GSUB'),
  1256. ttLib.getTableClass('GPOS'))
  1257. def remove_redundant_langsys(self):
  1258. table = self.table
  1259. if not table.ScriptList or not table.FeatureList:
  1260. return
  1261. features = table.FeatureList.FeatureRecord
  1262. for s in table.ScriptList.ScriptRecord:
  1263. d = s.Script.DefaultLangSys
  1264. if not d:
  1265. continue
  1266. for lr in s.Script.LangSysRecord[:]:
  1267. l = lr.LangSys
  1268. # Compare d and l
  1269. if len(d.FeatureIndex) != len(l.FeatureIndex):
  1270. continue
  1271. if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535):
  1272. continue
  1273. if d.ReqFeatureIndex != 65535:
  1274. if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]:
  1275. continue
  1276. for i in range(len(d.FeatureIndex)):
  1277. if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]:
  1278. break
  1279. else:
  1280. # LangSys and default are equal; delete LangSys
  1281. s.Script.LangSysRecord.remove(lr)
  1282. @_add_method(ttLib.getTableClass('GSUB'),
  1283. ttLib.getTableClass('GPOS'))
  1284. def prune_post_subset(self, options):
  1285. table = self.table
  1286. self.prune_lookups() # XXX Is this actually needed?!
  1287. if table.LookupList:
  1288. table.LookupList.prune_post_subset(options)
  1289. # XXX Next two lines disabled because OTS is stupid and
  1290. # doesn't like NULL offsets here.
  1291. #if not table.LookupList.Lookup:
  1292. # table.LookupList = None
  1293. if not table.LookupList:
  1294. table.FeatureList = None
  1295. if table.FeatureList:
  1296. self.remove_redundant_langsys()
  1297. # Remove unreferenced features
  1298. self.prune_features()
  1299. # XXX Next two lines disabled because OTS is stupid and
  1300. # doesn't like NULL offsets here.
  1301. #if table.FeatureList and not table.FeatureList.FeatureRecord:
  1302. # table.FeatureList = None
  1303. # Never drop scripts themselves as them just being available
  1304. # holds semantic significance.
  1305. # XXX Next two lines disabled because OTS is stupid and
  1306. # doesn't like NULL offsets here.
  1307. #if table.ScriptList and not table.ScriptList.ScriptRecord:
  1308. # table.ScriptList = None
  1309. return True
  1310. @_add_method(ttLib.getTableClass('GDEF'))
  1311. def subset_glyphs(self, s):
  1312. glyphs = s.glyphs_gsubed
  1313. table = self.table
  1314. if table.LigCaretList:
  1315. indices = table.LigCaretList.Coverage.subset(glyphs)
  1316. table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i]
  1317. for i in indices]
  1318. table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph)
  1319. if table.MarkAttachClassDef:
  1320. table.MarkAttachClassDef.classDefs = \
  1321. {g:v for g,v in table.MarkAttachClassDef.classDefs.items()
  1322. if g in glyphs}
  1323. if table.GlyphClassDef:
  1324. table.GlyphClassDef.classDefs = \
  1325. {g:v for g,v in table.GlyphClassDef.classDefs.items()
  1326. if g in glyphs}
  1327. if table.AttachList:
  1328. indices = table.AttachList.Coverage.subset(glyphs)
  1329. GlyphCount = table.AttachList.GlyphCount
  1330. table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i]
  1331. for i in indices
  1332. if i < GlyphCount]
  1333. table.AttachList.GlyphCount = len(table.AttachList.AttachPoint)
  1334. if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef:
  1335. for coverage in table.MarkGlyphSetsDef.Coverage:
  1336. coverage.subset(glyphs)
  1337. # TODO: The following is disabled. If enabling, we need to go fixup all
  1338. # lookups that use MarkFilteringSet and map their set.
  1339. # indices = table.MarkGlyphSetsDef.Coverage = \
  1340. # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs]
  1341. return True
  1342. @_add_method(ttLib.getTableClass('GDEF'))
  1343. def prune_post_subset(self, options):
  1344. table = self.table
  1345. # XXX check these against OTS
  1346. if table.LigCaretList and not table.LigCaretList.LigGlyphCount:
  1347. table.LigCaretList = None
  1348. if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs:
  1349. table.MarkAttachClassDef = None
  1350. if table.GlyphClassDef and not table.GlyphClassDef.classDefs:
  1351. table.GlyphClassDef = None
  1352. if table.AttachList and not table.AttachList.GlyphCount:
  1353. table.AttachList = None
  1354. if (hasattr(table, "MarkGlyphSetsDef") and
  1355. table.MarkGlyphSetsDef and
  1356. not table.MarkGlyphSetsDef.Coverage):
  1357. table.MarkGlyphSetsDef = None
  1358. if table.Version == 0x00010002/0x10000:
  1359. table.Version = 1.0
  1360. return bool(table.LigCaretList or
  1361. table.MarkAttachClassDef or
  1362. table.GlyphClassDef or
  1363. table.AttachList or
  1364. (table.Version >= 0x00010002/0x10000 and table.MarkGlyphSetsDef))
  1365. @_add_method(ttLib.getTableClass('kern'))
  1366. def prune_pre_subset(self, options):
  1367. # Prune unknown kern table types
  1368. self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')]
  1369. return bool(self.kernTables)
  1370. @_add_method(ttLib.getTableClass('kern'))
  1371. def subset_glyphs(self, s):
  1372. glyphs = s.glyphs_gsubed
  1373. for t in self.kernTables:
  1374. t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items()
  1375. if a in glyphs and b in glyphs}
  1376. self.kernTables = [t for t in self.kernTables if t.kernTable]
  1377. return bool(self.kernTables)
  1378. @_add_method(ttLib.getTableClass('vmtx'))
  1379. def subset_glyphs(self, s):
  1380. self.metrics = _dict_subset(self.metrics, s.glyphs)
  1381. return bool(self.metrics)
  1382. @_add_method(ttLib.getTableClass('hmtx'))
  1383. def subset_glyphs(self, s):
  1384. self.metrics = _dict_subset(self.metrics, s.glyphs)
  1385. return True # Required table
  1386. @_add_method(ttLib.getTableClass('hdmx'))
  1387. def subset_glyphs(self, s):
  1388. self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()}
  1389. return bool(self.hdmx)
  1390. @_add_method(ttLib.getTableClass('VORG'))
  1391. def subset_glyphs(self, s):
  1392. self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items()
  1393. if g in s.glyphs}
  1394. self.numVertOriginYMetrics = len(self.VOriginRecords)
  1395. return True # Never drop; has default metrics
  1396. @_add_method(ttLib.getTableClass('post'))
  1397. def prune_pre_subset(self, options):
  1398. if not options.glyph_names:
  1399. self.formatType = 3.0
  1400. return True # Required table
  1401. @_add_method(ttLib.getTableClass('post'))
  1402. def subset_glyphs(self, s):
  1403. self.extraNames = [] # This seems to do it
  1404. return True # Required table
  1405. @_add_method(ttLib.getTableModule('glyf').Glyph)
  1406. def remapComponentsFast(self, indices):
  1407. if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
  1408. return # Not composite
  1409. data = array.array("B", self.data)
  1410. i = 10
  1411. more = 1
  1412. while more:
  1413. flags =(data[i] << 8) | data[i+1]
  1414. glyphID =(data[i+2] << 8) | data[i+3]
  1415. # Remap
  1416. glyphID = indices.index(glyphID)
  1417. data[i+2] = glyphID >> 8
  1418. data[i+3] = glyphID & 0xFF
  1419. i += 4
  1420. flags = int(flags)
  1421. if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS
  1422. else: i += 2
  1423. if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE
  1424. elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE
  1425. elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO
  1426. more = flags & 0x0020 # MORE_COMPONENTS
  1427. self.data = data.tostring()
  1428. @_add_method(ttLib.getTableClass('glyf'))
  1429. def closure_glyphs(self, s):
  1430. decompose = s.glyphs
  1431. while True:
  1432. components = set()
  1433. for g in decompose:
  1434. if g not in self.glyphs:
  1435. continue
  1436. gl = self.glyphs[g]
  1437. for c in gl.getComponentNames(self):
  1438. if c not in s.glyphs:
  1439. components.add(c)
  1440. components = set(c for c in components if c not in s.glyphs)
  1441. if not components:
  1442. break
  1443. decompose = components
  1444. s.glyphs.update(components)
  1445. @_add_method(ttLib.getTableClass('glyf'))
  1446. def prune_pre_subset(self, options):
  1447. if options.notdef_glyph and not options.notdef_outline:
  1448. g = self[self.glyphOrder[0]]
  1449. # Yay, easy!
  1450. g.__dict__.clear()
  1451. g.data = ""
  1452. return True
  1453. @_add_method(ttLib.getTableClass('glyf'))
  1454. def subset_glyphs(self, s):
  1455. self.glyphs = _dict_subset(self.glyphs, s.glyphs)
  1456. indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs]
  1457. for v in self.glyphs.values():
  1458. if hasattr(v, "data"):
  1459. v.remapComponentsFast(indices)
  1460. else:
  1461. pass # No need
  1462. self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs]
  1463. # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
  1464. return True
  1465. @_add_method(ttLib.getTableClass('glyf'))
  1466. def prune_post_subset(self, options):
  1467. remove_hinting = not options.hinting
  1468. for v in self.glyphs.values():
  1469. v.trim(remove_hinting=remove_hinting)
  1470. return True
  1471. @_add_method(ttLib.getTableClass('CFF '))
  1472. def prune_pre_subset(self, options):
  1473. cff = self.cff
  1474. # CFF table must have one font only
  1475. cff.fontNames = cff.fontNames[:1]
  1476. if options.notdef_glyph and not options.notdef_outline:
  1477. for fontname in cff.keys():
  1478. font = cff[fontname]
  1479. c,_ = font.CharStrings.getItemAndSelector('.notdef')
  1480. # XXX we should preserve the glyph width
  1481. c.bytecode = '\x0e' # endchar
  1482. c.program = None
  1483. return True # bool(cff.fontNames)
  1484. @_add_method(ttLib.getTableClass('CFF '))
  1485. def subset_glyphs(self, s):
  1486. cff = self.cff
  1487. for fontname in cff.keys():
  1488. font = cff[fontname]
  1489. cs = font.CharStrings
  1490. # Load all glyphs
  1491. for g in font.charset:
  1492. if g not in s.glyphs: continue
  1493. c,sel = cs.getItemAndSelector(g)
  1494. if cs.charStringsAreIndexed:
  1495. indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
  1496. csi = cs.charStringsIndex
  1497. csi.items = [csi.items[i] for i in indices]
  1498. del csi.file, csi.offsets
  1499. if hasattr(font, "FDSelect"):
  1500. sel = font.FDSelect
  1501. # XXX We want to set sel.format to None, such that the
  1502. # most compact format is selected. However, OTS was
  1503. # broken and couldn't parse a FDSelect format 0 that
  1504. # happened before CharStrings. As such, always force
  1505. # format 3 until we fix cffLib to always generate
  1506. # FDSelect after CharStrings.
  1507. # https://github.com/khaledhosny/ots/pull/31
  1508. #sel.format = None
  1509. sel.format = 3
  1510. sel.gidArray = [sel.gidArray[i] for i in indices]
  1511. cs.charStrings = {g:indices.index(v)
  1512. for g,v in cs.charStrings.items()
  1513. if g in s.glyphs}
  1514. else:
  1515. cs.charStrings = {g:v
  1516. for g,v in cs.charStrings.items()
  1517. if g in s.glyphs}
  1518. font.charset = [g for g in font.charset if g in s.glyphs]
  1519. font.numGlyphs = len(font.charset)
  1520. return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
  1521. @_add_method(psCharStrings.T2CharString)
  1522. def subset_subroutines(self, subrs, gsubrs):
  1523. p = self.program
  1524. assert len(p)
  1525. for i in range(1, len(p)):
  1526. if p[i] == 'callsubr':
  1527. assert isinstance(p[i-1], int)
  1528. p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
  1529. elif p[i] == 'callgsubr':
  1530. assert isinstance(p[i-1], int)
  1531. p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
  1532. @_add_method(psCharStrings.T2CharString)
  1533. def drop_hints(self):
  1534. hints = self._hints
  1535. if hints.has_hint:
  1536. self.program = self.program[hints.last_hint:]
  1537. if hasattr(self, 'width'):
  1538. # Insert width back if needed
  1539. if self.width != self.private.defaultWidthX:
  1540. self.program.insert(0, self.width - self.private.nominalWidthX)
  1541. if hints.has_hintmask:
  1542. i = 0
  1543. p = self.program
  1544. while i < len(p):
  1545. if p[i] in ['hintmask', 'cntrmask']:
  1546. assert i + 1 <= len(p)
  1547. del p[i:i+2]
  1548. continue
  1549. i += 1
  1550. # TODO: we currently don't drop calls to "empty" subroutines.
  1551. assert len(self.program)
  1552. del self._hints
  1553. class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
  1554. def __init__(self, localSubrs, globalSubrs):
  1555. psCharStrings.SimpleT2Decompiler.__init__(self,
  1556. localSubrs,
  1557. globalSubrs)
  1558. for subrs in [localSubrs, globalSubrs]:
  1559. if subrs and not hasattr(subrs, "_used"):
  1560. subrs._used = set()
  1561. def op_callsubr(self, index):
  1562. self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
  1563. psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
  1564. def op_callgsubr(self, index):
  1565. self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
  1566. psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
  1567. class _DehintingT2Decompiler(psCharStrings.SimpleT2Decompiler):
  1568. class Hints(object):
  1569. def __init__(self):
  1570. # Whether calling this charstring produces any hint stems
  1571. self.has_hint = False
  1572. # Index to start at to drop all hints
  1573. self.last_hint = 0
  1574. # Index up to which we know more hints are possible.
  1575. # Only relevant if status is 0 or 1.
  1576. self.last_checked = 0
  1577. # The status means:
  1578. # 0: after dropping hints, this charstring is empty
  1579. # 1: after dropping hints, there may be more hints
  1580. # continuing after this
  1581. # 2: no more hints possible after this charstring
  1582. self.status = 0
  1583. # Has hintmask instructions; not recursive
  1584. self.has_hintmask = False
  1585. pass
  1586. def __init__(self, css, localSubrs, globalSubrs):
  1587. self._css = css
  1588. psCharStrings.SimpleT2Decompiler.__init__(self,
  1589. localSubrs,
  1590. globalSubrs)
  1591. def execute(self, charString):
  1592. old_hints = charString._hints if hasattr(charString, '_hints') else None
  1593. charString._hints = self.Hints()
  1594. psCharStrings.SimpleT2Decompiler.execute(self, charString)
  1595. hints = charString._hints
  1596. if hints.has_hint or hints.has_hintmask:
  1597. self._css.add(charString)
  1598. if hints.status != 2:
  1599. # Check from last_check, make sure we didn't have any operators.
  1600. for i in range(hints.last_checked, len(charString.program) - 1):
  1601. if isinstance(charString.program[i], str):
  1602. hints.status = 2
  1603. break
  1604. else:
  1605. hints.status = 1 # There's *something* here
  1606. hints.last_checked = len(charString.program)
  1607. if old_hints:
  1608. assert hints.__dict__ == old_hints.__dict__
  1609. def op_callsubr(self, index):
  1610. subr = self.localSubrs[self.operandStack[-1]+self.localBias]
  1611. psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
  1612. self.processSubr(index, subr)
  1613. def op_callgsubr(self, index):
  1614. subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
  1615. psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
  1616. self.processSubr(index, subr)
  1617. def op_hstem(self, index):
  1618. psCharStrings.SimpleT2Decompiler.op_hstem(self, index)
  1619. self.processHint(index)
  1620. def op_vstem(self, index):
  1621. psCharStrings.SimpleT2Decompiler.op_vstem(self, index)
  1622. self.processHint(index)
  1623. def op_hstemhm(self, index):
  1624. psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index)
  1625. self.processHint(index)
  1626. def op_vstemhm(self, index):
  1627. psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index)
  1628. self.processHint(index)
  1629. def op_hintmask(self, index):
  1630. psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
  1631. self.processHintmask(index)
  1632. def op_cntrmask(self, index):
  1633. psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index)
  1634. self.processHintmask(index)
  1635. def processHintmask(self, index):
  1636. cs = self.callingStack[-1]
  1637. hints = cs._hints
  1638. hints.has_hintmask = True
  1639. if hints.status != 2 and hints.has_hint:
  1640. # Check from last_check, see if we may be an implicit vstem
  1641. for i in range(hints.last_checked, index - 1):
  1642. if isinstance(cs.program[i], str):
  1643. hints.status = 2
  1644. break
  1645. if hints.status != 2:
  1646. # We are an implicit vstem
  1647. hints.last_hint = index + 1
  1648. hints.status = 0
  1649. hints.last_checked = index + 1
  1650. def processHint(self, index):
  1651. cs = self.callingStack[-1]
  1652. hints = cs._hints
  1653. hints.has_hint = True
  1654. hints.last_hint = index
  1655. hints.last_checked = index
  1656. def processSubr(self, index, subr):
  1657. cs = self.callingStack[-1]
  1658. hints = cs._hints
  1659. subr_hints = subr._hints
  1660. if subr_hints.has_hint:
  1661. if hints.status != 2:
  1662. hints.has_hint = True
  1663. hints.last_checked = index
  1664. hints.status = subr_hints.status
  1665. # Decide where to chop off from
  1666. if subr_hints.status == 0:
  1667. hints.last_hint = index
  1668. else:
  1669. hints.last_hint = index - 2 # Leave the subr call in
  1670. else:
  1671. # In my understanding, this is a font bug.
  1672. # I.e., it has hint stems *after* path construction.
  1673. # I've seen this in widespread fonts.
  1674. # Best to ignore the hints I suppose...
  1675. pass
  1676. #assert 0
  1677. else:
  1678. hints.status = max(hints.status, subr_hints.status)
  1679. if hints.status != 2:
  1680. # Check from last_check, make sure we didn't have
  1681. # any operators.
  1682. for i in range(hints.last_checked, index - 1):
  1683. if isinstance(cs.program[i], str):
  1684. hints.status = 2
  1685. break
  1686. hints.last_checked = index
  1687. if hints.status != 2:
  1688. # Decide where to chop off from
  1689. if subr_hints.status == 0:
  1690. hints.last_hint = index
  1691. else:
  1692. hints.last_hint = index - 2 # Leave the subr call in
  1693. class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
  1694. def __init__(self, localSubrs, globalSubrs):
  1695. psCharStrings.SimpleT2Decompiler.__init__(self,
  1696. localSubrs,
  1697. globalSubrs)
  1698. def execute(self, charString):
  1699. # Note: Currently we recompute _desubroutinized each time.
  1700. # This is more robust in some cases, but in other places we assume
  1701. # that each subroutine always expands to the same code, so
  1702. # maybe it doesn't matter. To speed up we can just not
  1703. # recompute _desubroutinized if it's there. For now I just
  1704. # double-check that it desubroutinized to the same thing.
  1705. old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None
  1706. charString._patches = []
  1707. psCharStrings.SimpleT2Decompiler.execute(self, charString)
  1708. desubroutinized = charString.program[:]
  1709. for idx,expansion in reversed (charString._patches):
  1710. assert idx >= 2
  1711. assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
  1712. assert type(desubroutinized[idx - 2]) == int
  1713. if expansion[-1] == 'return':
  1714. expansion = expansion[:-1]
  1715. desubroutinized[idx-2:idx] = expansion
  1716. if 'endchar' in desubroutinized:
  1717. # Cut off after first endchar
  1718. desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
  1719. else:
  1720. if not len(desubroutinized) or desubroutinized[-1] != 'return':
  1721. desubroutinized.append('return')
  1722. charString._desubroutinized = desubroutinized
  1723. del charString._patches
  1724. if old_desubroutinized:
  1725. assert desubroutinized == old_desubroutinized
  1726. def op_callsubr(self, index):
  1727. subr = self.localSubrs[self.operandStack[-1]+self.localBias]
  1728. psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
  1729. self.processSubr(index, subr)
  1730. def op_callgsubr(self, index):
  1731. subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
  1732. psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
  1733. self.processSubr(index, subr)
  1734. def processSubr(self, index, subr):
  1735. cs = self.callingStack[-1]
  1736. cs._patches.append((index, subr._desubroutinized))
  1737. @_add_method(ttLib.getTableClass('CFF '))
  1738. def prune_post_subset(self, options):
  1739. cff = self.cff
  1740. for fontname in cff.keys():
  1741. font = cff[fontname]
  1742. cs = font.CharStrings
  1743. # Drop unused FontDictionaries
  1744. if hasattr(font, "FDSelect"):
  1745. sel = font.FDSelect
  1746. indices = _uniq_sort(sel.gidArray)
  1747. sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
  1748. arr = font.FDArray
  1749. arr.items = [arr[i] for i in indices]
  1750. del arr.file, arr.offsets
  1751. # Desubroutinize if asked for
  1752. if options.desubroutinize:
  1753. for g in font.charset:
  1754. c,sel = cs.getItemAndSelector(g)
  1755. c.decompile()
  1756. subrs = getattr(c.private, "Subrs", [])
  1757. decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs)
  1758. decompiler.execute(c)
  1759. c.program = c._desubroutinized
  1760. # Drop hints if not needed
  1761. if not options.hinting:
  1762. # This can be tricky, but doesn't have to. What we do is:
  1763. #
  1764. # - Run all used glyph charstrings and recurse into subroutines,
  1765. # - For each charstring (including subroutines), if it has any
  1766. # of the hint stem operators, we mark it as such.
  1767. # Upon returning, for each charstring we note all the
  1768. # subroutine calls it makes that (recursively) contain a stem,
  1769. # - Dropping hinting then consists of the following two ops:
  1770. # * Drop the piece of the program in each charstring before the
  1771. # last call to a stem op or a stem-calling subroutine,
  1772. # * Drop all hintmask operations.
  1773. # - It's trickier... A hintmask right after hints and a few numbers
  1774. # will act as an implicit vstemhm. As such, we track whether
  1775. # we have seen any non-hint operators so far and do the right
  1776. # thing, recursively... Good luck understanding that :(
  1777. css = set()
  1778. for g in font.charset:
  1779. c,sel = cs.getItemAndSelector(g)
  1780. c.decompile()
  1781. subrs = getattr(c.private, "Subrs", [])
  1782. decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs)
  1783. decompiler.execute(c)
  1784. for charstring in css:
  1785. charstring.drop_hints()
  1786. del css
  1787. # Drop font-wide hinting values
  1788. all_privs = []
  1789. if hasattr(font, 'FDSelect'):
  1790. all_privs.extend(fd.Private for fd in font.FDArray)
  1791. else:
  1792. all_privs.append(font.Private)
  1793. for priv in all_privs:
  1794. for k in ['BlueValues', 'OtherBlues',
  1795. 'FamilyBlues', 'FamilyOtherBlues',
  1796. 'BlueScale', 'BlueShift', 'BlueFuzz',
  1797. 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']:
  1798. if hasattr(priv, k):
  1799. setattr(priv, k, None)
  1800. # Renumber subroutines to remove unused ones
  1801. # Mark all used subroutines
  1802. for g in font.charset:
  1803. c,sel = cs.getItemAndSelector(g)
  1804. subrs = getattr(c.private, "Subrs", [])
  1805. decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs)
  1806. decompiler.execute(c)
  1807. all_subrs = [font.GlobalSubrs]
  1808. if hasattr(font, 'FDSelect'):
  1809. all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
  1810. elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
  1811. all_subrs.append(font.Private.Subrs)
  1812. subrs = set(subrs) # Remove duplicates
  1813. # Prepare
  1814. for subrs in all_subrs:
  1815. if not hasattr(subrs, '_used'):
  1816. subrs._used = set()
  1817. subrs._used = _uniq_sort(subrs._used)
  1818. subrs._old_bias = psCharStrings.calcSubrBias(subrs)
  1819. subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
  1820. # Renumber glyph charstrings
  1821. for g in font.charset:
  1822. c,sel = cs.getItemAndSelector(g)
  1823. subrs = getattr(c.private, "Subrs", [])
  1824. c.subset_subroutines (subrs, font.GlobalSubrs)
  1825. # Renumber subroutines themselves
  1826. for subrs in all_subrs:
  1827. if subrs == font.GlobalSubrs:
  1828. if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'):
  1829. local_subrs = font.Private.Subrs
  1830. else:
  1831. local_subrs = []
  1832. else:
  1833. local_subrs = subrs
  1834. subrs.items = [subrs.items[i] for i in subrs._used]
  1835. del subrs.file
  1836. if hasattr(subrs, 'offsets'):
  1837. del subrs.offsets
  1838. for subr in subrs.items:
  1839. subr.subset_subroutines (local_subrs, font.GlobalSubrs)
  1840. # Cleanup
  1841. for subrs in all_subrs:
  1842. del subrs._used, subrs._old_bias, subrs._new_bias
  1843. return True
  1844. @_add_method(ttLib.getTableClass('cmap'))
  1845. def closure_glyphs(self, s):
  1846. tables = [t for t in self.tables if t.isUnicode()]
  1847. # Close glyphs
  1848. for table in tables:
  1849. if table.format == 14:
  1850. for cmap in table.uvsDict.values():
  1851. glyphs = {g for u,g in cmap if u in s.unicodes_requested}
  1852. if None in glyphs:
  1853. glyphs.remove(None)
  1854. s.glyphs.update(glyphs)
  1855. else:
  1856. cmap = table.cmap
  1857. intersection = s.unicodes_requested.intersection(cmap.keys())
  1858. s.glyphs.update(cmap[u] for u in intersection)
  1859. # Calculate unicodes_missing
  1860. s.unicodes_missing = s.unicodes_requested.copy()
  1861. for table in tables:
  1862. s.unicodes_missing.difference_update(table.cmap)
  1863. @_add_method(ttLib.getTableClass('cmap'))
  1864. def prune_pre_subset(self, options):
  1865. if not options.legacy_cmap:
  1866. # Drop non-Unicode / non-Symbol cmaps
  1867. self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()]
  1868. if not options.symbol_cmap:
  1869. self.tables = [t for t in self.tables if not t.isSymbol()]
  1870. # TODO(behdad) Only keep one subtable?
  1871. # For now, drop format=0 which can't be subset_glyphs easily?
  1872. self.tables = [t for t in self.tables if t.format != 0]
  1873. self.numSubTables = len(self.tables)
  1874. return True # Required table
  1875. @_add_method(ttLib.getTableClass('cmap'))
  1876. def subset_glyphs(self, s):
  1877. s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only
  1878. for t in self.tables:
  1879. if t.format == 14:
  1880. # TODO(behdad) We drop all the default-UVS mappings
  1881. # for glyphs_requested. So it's the caller's responsibility to make
  1882. # sure those are included.
  1883. t.uvsDict = {v:[(u,g) for u,g in l
  1884. if g in s.glyphs_requested or u in s.unicodes_requested]
  1885. for v,l in t.uvsDict.items()}
  1886. t.uvsDict = {v:l for v,l in t.uvsDict.items() if l}
  1887. elif t.isUnicode():
  1888. t.cmap = {u:g for u,g in t.cmap.items()
  1889. if g in s.glyphs_requested or u in s.unicodes_requested}
  1890. else:
  1891. t.cmap = {u:g for u,g in t.cmap.items()
  1892. if g in s.glyphs_requested}
  1893. self.tables = [t for t in self.tables
  1894. if (t.cmap if t.format != 14 else t.uvsDict)]
  1895. self.numSubTables = len(self.tables)
  1896. # TODO(behdad) Convert formats when needed.
  1897. # In particular, if we have a format=12 without non-BMP
  1898. # characters, either drop format=12 one or convert it
  1899. # to format=4 if there's not one.
  1900. return True # Required table
  1901. @_add_method(ttLib.getTableClass('DSIG'))
  1902. def prune_pre_subset(self, options):
  1903. # Drop all signatures since they will be invalid
  1904. self.usNumSigs = 0
  1905. self.signatureRecords = []
  1906. return True
  1907. @_add_method(ttLib.getTableClass('maxp'))
  1908. def prune_pre_subset(self, options):
  1909. if not options.hinting:
  1910. if self.tableVersion == 0x00010000:
  1911. self.maxZones = 1
  1912. self.maxTwilightPoints = 0
  1913. self.maxFunctionDefs = 0
  1914. self.maxInstructionDefs = 0
  1915. self.maxStackElements = 0
  1916. self.maxSizeOfInstructions = 0
  1917. return True
  1918. @_add_method(ttLib.getTableClass('name'))
  1919. def prune_pre_subset(self, options):
  1920. if '*' not in options.name_IDs:
  1921. self.names = [n for n in self.names if n.nameID in options.name_IDs]
  1922. if not options.name_legacy:
  1923. # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman
  1924. # entry for Latin and no Unicode names.
  1925. self.names = [n for n in self.names if n.isUnicode()]
  1926. # TODO(behdad) Option to keep only one platform's
  1927. if '*' not in options.name_languages:
  1928. # TODO(behdad) This is Windows-platform specific!
  1929. self.names = [n for n in self.names
  1930. if n.langID in options.name_languages]
  1931. if options.obfuscate_names:
  1932. namerecs = []
  1933. for n in self.names:
  1934. if n.nameID in [1, 4]:
  1935. n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f"
  1936. elif n.nameID in [2, 6]:
  1937. n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f"
  1938. elif n.nameID == 3:
  1939. n.string = ""
  1940. elif n.nameID in [16, 17, 18]:
  1941. continue
  1942. namerecs.append(n)
  1943. self.names = namerecs
  1944. return True # Required table
  1945. # TODO(behdad) OS/2 ulUnicodeRange / ulCodePageRange?
  1946. # TODO(behdad) Drop AAT tables.
  1947. # TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries.
  1948. # TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left
  1949. # TODO(behdad) Drop GDEF subitems if unused by lookups
  1950. # TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF)
  1951. # TODO(behdad) Text direction considerations.
  1952. # TODO(behdad) Text script / language considerations.
  1953. # TODO(behdad) Optionally drop 'kern' table if GPOS available
  1954. # TODO(behdad) Implement --unicode='*' to choose all cmap'ed
  1955. # TODO(behdad) Drop old-spec Indic scripts
  1956. class Options(object):
  1957. class OptionError(Exception): pass
  1958. class UnknownOptionError(OptionError): pass
  1959. _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC',
  1960. 'EBSC', 'SVG ', 'PCLT', 'LTSH']
  1961. _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite
  1962. _drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color
  1963. _no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp',
  1964. 'vhea', 'OS/2', 'loca', 'name', 'cvt ',
  1965. 'fpgm', 'prep', 'VDMX', 'DSIG']
  1966. _hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX']
  1967. # Based on HarfBuzz shapers
  1968. _layout_features_groups = {
  1969. # Default shaper
  1970. 'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'],
  1971. 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'],
  1972. 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'],
  1973. 'ltr': ['ltra', 'ltrm'],
  1974. 'rtl': ['rtla', 'rtlm'],
  1975. # Complex shapers
  1976. 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3',
  1977. 'cswh', 'mset'],
  1978. 'hangul': ['ljmo', 'vjmo', 'tjmo'],
  1979. 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'],
  1980. 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half',
  1981. 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres',
  1982. 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'],
  1983. }
  1984. _layout_features_default = _uniq_sort(sum(
  1985. iter(_layout_features_groups.values()), []))
  1986. drop_tables = _drop_tables_default
  1987. no_subset_tables = _no_subset_tables_default
  1988. hinting_tables = _hinting_tables_default
  1989. legacy_kern = False # drop 'kern' table if GPOS available
  1990. layout_features = _layout_features_default
  1991. ignore_missing_glyphs = False
  1992. ignore_missing_unicodes = True
  1993. hinting = True
  1994. glyph_names = False
  1995. legacy_cmap = False
  1996. symbol_cmap = False
  1997. name_IDs = [1, 2] # Family and Style
  1998. name_legacy = False
  1999. name_languages = [0x0409] # English
  2000. obfuscate_names = False # to make webfont unusable as a system font
  2001. notdef_glyph = True # gid0 for TrueType / .notdef for CFF
  2002. notdef_outline = False # No need for notdef to have an outline really
  2003. recommended_glyphs = False # gid1, gid2, gid3 for TrueType
  2004. recalc_bounds = False # Recalculate font bounding boxes
  2005. recalc_timestamp = False # Recalculate font modified timestamp
  2006. canonical_order = False # Order tables as recommended
  2007. flavor = None # May be 'woff' or 'woff2'
  2008. desubroutinize = False # Desubroutinize CFF CharStrings
  2009. def __init__(self, **kwargs):
  2010. self.set(**kwargs)
  2011. def set(self, **kwargs):
  2012. for k,v in kwargs.items():
  2013. if not hasattr(self, k):
  2014. raise self.UnknownOptionError("Unknown option '%s'" % k)
  2015. setattr(self, k, v)
  2016. def parse_opts(self, argv, ignore_unknown=False):
  2017. ret = []
  2018. for a in argv:
  2019. orig_a = a
  2020. if not a.startswith('--'):
  2021. ret.append(a)
  2022. continue
  2023. a = a[2:]
  2024. i = a.find('=')
  2025. op = '='
  2026. if i == -1:
  2027. if a.startswith("no-"):
  2028. k = a[3:]
  2029. v = False
  2030. else:
  2031. k = a
  2032. v = True
  2033. if k.endswith("?"):
  2034. k = k[:-1]
  2035. v = '?'
  2036. else:
  2037. k = a[:i]
  2038. if k[-1] in "-+":
  2039. op = k[-1]+'=' # Op is '-=' or '+=' now.
  2040. k = k[:-1]
  2041. v = a[i+1:]
  2042. ok = k
  2043. k = k.replace('-', '_')
  2044. if not hasattr(self, k):
  2045. if ignore_unknown is True or ok in ignore_unknown:
  2046. ret.append(orig_a)
  2047. continue
  2048. else:
  2049. raise self.UnknownOptionError("Unknown option '%s'" % a)
  2050. ov = getattr(self, k)
  2051. if v == '?':
  2052. print("Current setting for '%s' is: %s" % (ok, ov))
  2053. continue
  2054. if isinstance(ov, bool):
  2055. v = bool(v)
  2056. elif isinstance(ov, int):
  2057. v = int(v)
  2058. elif isinstance(ov, str):
  2059. v = str(v) # redundant
  2060. elif isinstance(ov, list):
  2061. if isinstance(v, bool):
  2062. raise self.OptionError("Option '%s' requires values to be specified using '='" % a)
  2063. vv = v.replace(',', ' ').split()
  2064. if vv == ['']:
  2065. vv = []
  2066. vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
  2067. if op == '=':
  2068. v = vv
  2069. elif op == '+=':
  2070. v = ov
  2071. v.extend(vv)
  2072. elif op == '-=':
  2073. v = ov
  2074. for x in vv:
  2075. if x in v:
  2076. v.remove(x)
  2077. else:
  2078. assert False
  2079. setattr(self, k, v)
  2080. return ret
  2081. class Subsetter(object):
  2082. class SubsettingError(Exception): pass
  2083. class MissingGlyphsSubsettingError(SubsettingError): pass
  2084. class MissingUnicodesSubsettingError(SubsettingError): pass
  2085. def __init__(self, options=None, log=None):
  2086. if not log:
  2087. log = Logger()
  2088. if not options:
  2089. options = Options()
  2090. self.options = options
  2091. self.log = log
  2092. self.unicodes_requested = set()
  2093. self.glyph_names_requested = set()
  2094. self.glyph_ids_requested = set()
  2095. def populate(self, glyphs=[], gids=[], unicodes=[], text=""):
  2096. self.unicodes_requested.update(unicodes)
  2097. if isinstance(text, bytes):
  2098. text = text.decode("utf_8")
  2099. for u in text:
  2100. self.unicodes_requested.add(ord(u))
  2101. self.glyph_names_requested.update(glyphs)
  2102. self.glyph_ids_requested.update(gids)
  2103. def _prune_pre_subset(self, font):
  2104. for tag in font.keys():
  2105. if tag == 'GlyphOrder': continue
  2106. if(tag in self.options.drop_tables or
  2107. (tag in self.options.hinting_tables and not self.options.hinting) or
  2108. (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))):
  2109. self.log(tag, "dropped")
  2110. del font[tag]
  2111. continue
  2112. clazz = ttLib.getTableClass(tag)
  2113. if hasattr(clazz, 'prune_pre_subset'):
  2114. table = font[tag]
  2115. self.log.lapse("load '%s'" % tag)
  2116. retain = table.prune_pre_subset(self.options)
  2117. self.log.lapse("prune '%s'" % tag)
  2118. if not retain:
  2119. self.log(tag, "pruned to empty; dropped")
  2120. del font[tag]
  2121. continue
  2122. else:
  2123. self.log(tag, "pruned")
  2124. def _closure_glyphs(self, font):
  2125. realGlyphs = set(font.getGlyphOrder())
  2126. glyph_order = font.getGlyphOrder()
  2127. self.glyphs_requested = set()
  2128. self.glyphs_requested.update(self.glyph_names_requested)
  2129. self.glyphs_requested.update(glyph_order[i]
  2130. for i in self.glyph_ids_requested
  2131. if i < len(glyph_order))
  2132. self.glyphs_missing = set()
  2133. self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs))
  2134. self.glyphs_missing.update(i for i in self.glyph_ids_requested
  2135. if i >= len(glyph_order))
  2136. if self.glyphs_missing:
  2137. self.log("Missing requested glyphs: %s" % self.glyphs_missing)
  2138. if not self.options.ignore_missing_glyphs:
  2139. raise self.MissingGlyphsSubsettingError(self.glyphs_missing)
  2140. self.glyphs = self.glyphs_requested.copy()
  2141. self.unicodes_missing = set()
  2142. if 'cmap' in font:
  2143. font['cmap'].closure_glyphs(self)
  2144. self.glyphs.intersection_update(realGlyphs)
  2145. self.log.lapse("close glyph list over 'cmap'")
  2146. self.glyphs_cmaped = frozenset(self.glyphs)
  2147. if self.unicodes_missing:
  2148. missing = ["U+%04X" % u for u in self.unicodes_missing]
  2149. self.log("Missing glyphs for requested Unicodes: %s" % missing)
  2150. if not self.options.ignore_missing_unicodes:
  2151. raise self.MissingUnicodesSubsettingError(missing)
  2152. del missing
  2153. if self.options.notdef_glyph:
  2154. if 'glyf' in font:
  2155. self.glyphs.add(font.getGlyphName(0))
  2156. self.log("Added gid0 to subset")
  2157. else:
  2158. self.glyphs.add('.notdef')
  2159. self.log("Added .notdef to subset")
  2160. if self.options.recommended_glyphs:
  2161. if 'glyf' in font:
  2162. for i in range(min(4, len(font.getGlyphOrder()))):
  2163. self.glyphs.add(font.getGlyphName(i))
  2164. self.log("Added first four glyphs to subset")
  2165. if 'GSUB' in font:
  2166. self.log("Closing glyph list over 'GSUB': %d glyphs before" %
  2167. len(self.glyphs))
  2168. self.log.glyphs(self.glyphs, font=font)
  2169. font['GSUB'].closure_glyphs(self)
  2170. self.glyphs.intersection_update(realGlyphs)
  2171. self.log("Closed glyph list over 'GSUB': %d glyphs after" %
  2172. len(self.glyphs))
  2173. self.log.glyphs(self.glyphs, font=font)
  2174. self.log.lapse("close glyph list over 'GSUB'")
  2175. self.glyphs_gsubed = frozenset(self.glyphs)
  2176. if 'glyf' in font:
  2177. self.log("Closing glyph list over 'glyf': %d glyphs before" %
  2178. len(self.glyphs))
  2179. self.log.glyphs(self.glyphs, font=font)
  2180. font['glyf'].closure_glyphs(self)
  2181. self.glyphs.intersection_update(realGlyphs)
  2182. self.log("Closed glyph list over 'glyf': %d glyphs after" %
  2183. len(self.glyphs))
  2184. self.log.glyphs(self.glyphs, font=font)
  2185. self.log.lapse("close glyph list over 'glyf'")
  2186. self.glyphs_glyfed = frozenset(self.glyphs)
  2187. self.glyphs_all = frozenset(self.glyphs)
  2188. self.log("Retaining %d glyphs: " % len(self.glyphs_all))
  2189. del self.glyphs
  2190. def _subset_glyphs(self, font):
  2191. for tag in font.keys():
  2192. if tag == 'GlyphOrder': continue
  2193. clazz = ttLib.getTableClass(tag)
  2194. if tag in self.options.no_subset_tables:
  2195. self.log(tag, "subsetting not needed")
  2196. elif hasattr(clazz, 'subset_glyphs'):
  2197. table = font[tag]
  2198. self.glyphs = self.glyphs_all
  2199. retain = table.subset_glyphs(self)
  2200. del self.glyphs
  2201. self.log.lapse("subset '%s'" % tag)
  2202. if not retain:
  2203. self.log(tag, "subsetted to empty; dropped")
  2204. del font[tag]
  2205. else:
  2206. self.log(tag, "subsetted")
  2207. else:
  2208. self.log(tag, "NOT subset; don't know how to subset; dropped")
  2209. del font[tag]
  2210. glyphOrder = font.getGlyphOrder()
  2211. glyphOrder = [g for g in glyphOrder if g in self.glyphs_all]
  2212. font.setGlyphOrder(glyphOrder)
  2213. font._buildReverseGlyphOrderDict()
  2214. self.log.lapse("subset GlyphOrder")
  2215. def _prune_post_subset(self, font):
  2216. for tag in font.keys():
  2217. if tag == 'GlyphOrder': continue
  2218. clazz = ttLib.getTableClass(tag)
  2219. if hasattr(clazz, 'prune_post_subset'):
  2220. table = font[tag]
  2221. retain = table.prune_post_subset(self.options)
  2222. self.log.lapse("prune '%s'" % tag)
  2223. if not retain:
  2224. self.log(tag, "pruned to empty; dropped")
  2225. del font[tag]
  2226. else:
  2227. self.log(tag, "pruned")
  2228. def subset(self, font):
  2229. self._prune_pre_subset(font)
  2230. self._closure_glyphs(font)
  2231. self._subset_glyphs(font)
  2232. self._prune_post_subset(font)
  2233. class Logger(object):
  2234. def __init__(self, verbose=False, xml=False, timing=False):
  2235. self.verbose = verbose
  2236. self.xml = xml
  2237. self.timing = timing
  2238. self.last_time = self.start_time = time.time()
  2239. def parse_opts(self, argv):
  2240. argv = argv[:]
  2241. for v in ['verbose', 'xml', 'timing']:
  2242. if "--"+v in argv:
  2243. setattr(self, v, True)
  2244. argv.remove("--"+v)
  2245. return argv
  2246. def __call__(self, *things):
  2247. if not self.verbose:
  2248. return
  2249. print(' '.join(str(x) for x in things))
  2250. def lapse(self, *things):
  2251. if not self.timing:
  2252. return
  2253. new_time = time.time()
  2254. print("Took %0.3fs to %s" %(new_time - self.last_time,
  2255. ' '.join(str(x) for x in things)))
  2256. self.last_time = new_time
  2257. def glyphs(self, glyphs, font=None):
  2258. if not self.verbose:
  2259. return
  2260. self("Glyph names:", sorted(glyphs))
  2261. if font:
  2262. reverseGlyphMap = font.getReverseGlyphMap()
  2263. self("Glyph IDs: ", sorted(reverseGlyphMap[g] for g in glyphs))
  2264. def font(self, font, file=sys.stdout):
  2265. if not self.xml:
  2266. return
  2267. from fontTools.misc import xmlWriter
  2268. writer = xmlWriter.XMLWriter(file)
  2269. for tag in font.keys():
  2270. writer.begintag(tag)
  2271. writer.newline()
  2272. font[tag].toXML(writer, font)
  2273. writer.endtag(tag)
  2274. writer.newline()
  2275. def load_font(fontFile,
  2276. options,
  2277. allowVID=False,
  2278. checkChecksums=False,
  2279. dontLoadGlyphNames=False,
  2280. lazy=True):
  2281. font = ttLib.TTFont(fontFile,
  2282. allowVID=allowVID,
  2283. checkChecksums=checkChecksums,
  2284. recalcBBoxes=options.recalc_bounds,
  2285. recalcTimestamp=options.recalc_timestamp,
  2286. lazy=lazy)
  2287. # Hack:
  2288. #
  2289. # If we don't need glyph names, change 'post' class to not try to
  2290. # load them. It avoid lots of headache with broken fonts as well
  2291. # as loading time.
  2292. #
  2293. # Ideally ttLib should provide a way to ask it to skip loading
  2294. # glyph names. But it currently doesn't provide such a thing.
  2295. #
  2296. if dontLoadGlyphNames:
  2297. post = ttLib.getTableClass('post')
  2298. saved = post.decode_format_2_0
  2299. post.decode_format_2_0 = post.decode_format_3_0
  2300. f = font['post']
  2301. if f.formatType == 2.0:
  2302. f.formatType = 3.0
  2303. post.decode_format_2_0 = saved
  2304. return font
  2305. def save_font(font, outfile, options):
  2306. if options.flavor and not hasattr(font, 'flavor'):
  2307. raise Exception("fonttools version does not support flavors.")
  2308. font.flavor = options.flavor
  2309. font.save(outfile, reorderTables=options.canonical_order)
  2310. def parse_unicodes(s):
  2311. import re
  2312. s = re.sub (r"0[xX]", " ", s)
  2313. s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s)
  2314. l = []
  2315. for item in s.split():
  2316. fields = item.split('-')
  2317. if len(fields) == 1:
  2318. l.append(int(item, 16))
  2319. else:
  2320. start,end = fields
  2321. l.extend(range(int(start, 16), int(end, 16)+1))
  2322. return l
  2323. def parse_gids(s):
  2324. l = []
  2325. for item in s.replace(',', ' ').split():
  2326. fields = item.split('-')
  2327. if len(fields) == 1:
  2328. l.append(int(fields[0]))
  2329. else:
  2330. l.extend(range(int(fields[0]), int(fields[1])+1))
  2331. return l
  2332. def parse_glyphs(s):
  2333. return s.replace(',', ' ').split()
  2334. def main(args=None):
  2335. if args is None:
  2336. args = sys.argv[1:]
  2337. if '--help' in args:
  2338. print(__doc__)
  2339. sys.exit(0)
  2340. log = Logger()
  2341. args = log.parse_opts(args)
  2342. options = Options()
  2343. args = options.parse_opts(args,
  2344. ignore_unknown=['gids', 'gids-file',
  2345. 'glyphs', 'glyphs-file',
  2346. 'text', 'text-file',
  2347. 'unicodes', 'unicodes-file',
  2348. 'output-file'])
  2349. if len(args) < 2:
  2350. print("usage:", __usage__, file=sys.stderr)
  2351. print("Try pyftsubset --help for more information.", file=sys.stderr)
  2352. sys.exit(1)
  2353. fontfile = args[0]
  2354. args = args[1:]
  2355. subsetter = Subsetter(options=options, log=log)
  2356. outfile = fontfile + '.subset'
  2357. glyphs = []
  2358. gids = []
  2359. unicodes = []
  2360. wildcard_glyphs = False
  2361. wildcard_unicodes = False
  2362. text = ""
  2363. for g in args:
  2364. if g == '*':
  2365. wildcard_glyphs = True
  2366. continue
  2367. if g.startswith('--output-file='):
  2368. outfile = g[14:]
  2369. continue
  2370. if g.startswith('--text='):
  2371. text += g[7:]
  2372. continue
  2373. if g.startswith('--text-file='):
  2374. text += open(g[12:]).read().replace('\n', '')
  2375. continue
  2376. if g.startswith('--unicodes='):
  2377. if g[11:] == '*':
  2378. wildcard_unicodes = True
  2379. else:
  2380. unicodes.extend(parse_unicodes(g[11:]))
  2381. continue
  2382. if g.startswith('--unicodes-file='):
  2383. for line in open(g[16:]).readlines():
  2384. unicodes.extend(parse_unicodes(line.split('#')[0]))
  2385. continue
  2386. if g.startswith('--gids='):
  2387. gids.extend(parse_gids(g[7:]))
  2388. continue
  2389. if g.startswith('--gids-file='):
  2390. for line in open(g[12:]).readlines():
  2391. gids.extend(parse_gids(line.split('#')[0]))
  2392. continue
  2393. if g.startswith('--glyphs='):
  2394. if g[9:] == '*':
  2395. wildcard_glyphs = True
  2396. else:
  2397. glyphs.extend(parse_glyphs(g[9:]))
  2398. continue
  2399. if g.startswith('--glyphs-file='):
  2400. for line in open(g[14:]).readlines():
  2401. glyphs.extend(parse_glyphs(line.split('#')[0]))
  2402. continue
  2403. glyphs.append(g)
  2404. dontLoadGlyphNames = not options.glyph_names and not glyphs
  2405. font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
  2406. log.lapse("load font")
  2407. if wildcard_glyphs:
  2408. glyphs.extend(font.getGlyphOrder())
  2409. if wildcard_unicodes:
  2410. for t in font['cmap'].tables:
  2411. if t.isUnicode():
  2412. unicodes.extend(t.cmap.keys())
  2413. assert '' not in glyphs
  2414. log.lapse("compile glyph list")
  2415. log("Text: '%s'" % text)
  2416. log("Unicodes:", unicodes)
  2417. log("Glyphs:", glyphs)
  2418. log("Gids:", gids)
  2419. subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text)
  2420. subsetter.subset(font)
  2421. save_font (font, outfile, options)
  2422. log.lapse("compile and save font")
  2423. log.last_time = log.start_time
  2424. log.lapse("make one with everything(TOTAL TIME)")
  2425. if log.verbose:
  2426. import os
  2427. log("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile))
  2428. log("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile))
  2429. log.font(font)
  2430. font.close()
  2431. __all__ = [
  2432. 'Options',
  2433. 'Subsetter',
  2434. 'Logger',
  2435. 'load_font',
  2436. 'save_font',
  2437. 'parse_gids',
  2438. 'parse_glyphs',
  2439. 'parse_unicodes',
  2440. 'main'
  2441. ]
  2442. if __name__ == '__main__':
  2443. main()