layout.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. # Copyright 2013 Google, Inc. All Rights Reserved.
  2. #
  3. # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
  4. from fontTools import ttLib
  5. from fontTools.ttLib.tables.DefaultTable import DefaultTable
  6. from fontTools.ttLib.tables import otTables
  7. from fontTools.merge.base import add_method, mergeObjects
  8. from fontTools.merge.util import *
  9. import logging
  10. log = logging.getLogger("fontTools.merge")
  11. def mergeLookupLists(lst):
  12. # TODO Do smarter merge.
  13. return sumLists(lst)
  14. def mergeFeatures(lst):
  15. assert lst
  16. self = otTables.Feature()
  17. self.FeatureParams = None
  18. self.LookupListIndex = mergeLookupLists(
  19. [l.LookupListIndex for l in lst if l.LookupListIndex]
  20. )
  21. self.LookupCount = len(self.LookupListIndex)
  22. return self
  23. def mergeFeatureLists(lst):
  24. d = {}
  25. for l in lst:
  26. for f in l:
  27. tag = f.FeatureTag
  28. if tag not in d:
  29. d[tag] = []
  30. d[tag].append(f.Feature)
  31. ret = []
  32. for tag in sorted(d.keys()):
  33. rec = otTables.FeatureRecord()
  34. rec.FeatureTag = tag
  35. rec.Feature = mergeFeatures(d[tag])
  36. ret.append(rec)
  37. return ret
  38. def mergeLangSyses(lst):
  39. assert lst
  40. # TODO Support merging ReqFeatureIndex
  41. assert all(l.ReqFeatureIndex == 0xFFFF for l in lst)
  42. self = otTables.LangSys()
  43. self.LookupOrder = None
  44. self.ReqFeatureIndex = 0xFFFF
  45. self.FeatureIndex = mergeFeatureLists(
  46. [l.FeatureIndex for l in lst if l.FeatureIndex]
  47. )
  48. self.FeatureCount = len(self.FeatureIndex)
  49. return self
  50. def mergeScripts(lst):
  51. assert lst
  52. if len(lst) == 1:
  53. return lst[0]
  54. langSyses = {}
  55. for sr in lst:
  56. for lsr in sr.LangSysRecord:
  57. if lsr.LangSysTag not in langSyses:
  58. langSyses[lsr.LangSysTag] = []
  59. langSyses[lsr.LangSysTag].append(lsr.LangSys)
  60. lsrecords = []
  61. for tag, langSys_list in sorted(langSyses.items()):
  62. lsr = otTables.LangSysRecord()
  63. lsr.LangSys = mergeLangSyses(langSys_list)
  64. lsr.LangSysTag = tag
  65. lsrecords.append(lsr)
  66. self = otTables.Script()
  67. self.LangSysRecord = lsrecords
  68. self.LangSysCount = len(lsrecords)
  69. dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys]
  70. if dfltLangSyses:
  71. self.DefaultLangSys = mergeLangSyses(dfltLangSyses)
  72. else:
  73. self.DefaultLangSys = None
  74. return self
  75. def mergeScriptRecords(lst):
  76. d = {}
  77. for l in lst:
  78. for s in l:
  79. tag = s.ScriptTag
  80. if tag not in d:
  81. d[tag] = []
  82. d[tag].append(s.Script)
  83. ret = []
  84. for tag in sorted(d.keys()):
  85. rec = otTables.ScriptRecord()
  86. rec.ScriptTag = tag
  87. rec.Script = mergeScripts(d[tag])
  88. ret.append(rec)
  89. return ret
  90. otTables.ScriptList.mergeMap = {
  91. "ScriptCount": lambda lst: None, # TODO
  92. "ScriptRecord": mergeScriptRecords,
  93. }
  94. otTables.BaseScriptList.mergeMap = {
  95. "BaseScriptCount": lambda lst: None, # TODO
  96. # TODO: Merge duplicate entries
  97. "BaseScriptRecord": lambda lst: sorted(
  98. sumLists(lst), key=lambda s: s.BaseScriptTag
  99. ),
  100. }
  101. otTables.FeatureList.mergeMap = {
  102. "FeatureCount": sum,
  103. "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag),
  104. }
  105. otTables.LookupList.mergeMap = {
  106. "LookupCount": sum,
  107. "Lookup": sumLists,
  108. }
  109. otTables.Coverage.mergeMap = {
  110. "Format": min,
  111. "glyphs": sumLists,
  112. }
  113. otTables.ClassDef.mergeMap = {
  114. "Format": min,
  115. "classDefs": sumDicts,
  116. }
  117. otTables.LigCaretList.mergeMap = {
  118. "Coverage": mergeObjects,
  119. "LigGlyphCount": sum,
  120. "LigGlyph": sumLists,
  121. }
  122. otTables.AttachList.mergeMap = {
  123. "Coverage": mergeObjects,
  124. "GlyphCount": sum,
  125. "AttachPoint": sumLists,
  126. }
  127. # XXX Renumber MarkFilterSets of lookups
  128. otTables.MarkGlyphSetsDef.mergeMap = {
  129. "MarkSetTableFormat": equal,
  130. "MarkSetCount": sum,
  131. "Coverage": sumLists,
  132. }
  133. otTables.Axis.mergeMap = {
  134. "*": mergeObjects,
  135. }
  136. # XXX Fix BASE table merging
  137. otTables.BaseTagList.mergeMap = {
  138. "BaseTagCount": sum,
  139. "BaselineTag": sumLists,
  140. }
  141. otTables.GDEF.mergeMap = otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = (
  142. otTables.BASE.mergeMap
  143. ) = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = {
  144. "*": mergeObjects,
  145. "Version": max,
  146. }
  147. ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass("GSUB").mergeMap = (
  148. ttLib.getTableClass("GPOS").mergeMap
  149. ) = ttLib.getTableClass("BASE").mergeMap = ttLib.getTableClass(
  150. "JSTF"
  151. ).mergeMap = ttLib.getTableClass(
  152. "MATH"
  153. ).mergeMap = {
  154. "tableTag": onlyExisting(equal), # XXX clean me up
  155. "table": mergeObjects,
  156. }
  157. @add_method(ttLib.getTableClass("GSUB"))
  158. def merge(self, m, tables):
  159. assert len(tables) == len(m.duplicateGlyphsPerFont)
  160. for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
  161. if not dups:
  162. continue
  163. if table is None or table is NotImplemented:
  164. log.warning(
  165. "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s",
  166. m.fonts[i]._merger__name,
  167. dups,
  168. )
  169. continue
  170. synthFeature = None
  171. synthLookup = None
  172. for script in table.table.ScriptList.ScriptRecord:
  173. if script.ScriptTag == "DFLT":
  174. continue # XXX
  175. for langsys in [script.Script.DefaultLangSys] + [
  176. l.LangSys for l in script.Script.LangSysRecord
  177. ]:
  178. if langsys is None:
  179. continue # XXX Create!
  180. feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"]
  181. assert len(feature) <= 1
  182. if feature:
  183. feature = feature[0]
  184. else:
  185. if not synthFeature:
  186. synthFeature = otTables.FeatureRecord()
  187. synthFeature.FeatureTag = "locl"
  188. f = synthFeature.Feature = otTables.Feature()
  189. f.FeatureParams = None
  190. f.LookupCount = 0
  191. f.LookupListIndex = []
  192. table.table.FeatureList.FeatureRecord.append(synthFeature)
  193. table.table.FeatureList.FeatureCount += 1
  194. feature = synthFeature
  195. langsys.FeatureIndex.append(feature)
  196. langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag)
  197. if not synthLookup:
  198. subtable = otTables.SingleSubst()
  199. subtable.mapping = dups
  200. synthLookup = otTables.Lookup()
  201. synthLookup.LookupFlag = 0
  202. synthLookup.LookupType = 1
  203. synthLookup.SubTableCount = 1
  204. synthLookup.SubTable = [subtable]
  205. if table.table.LookupList is None:
  206. # mtiLib uses None as default value for LookupList,
  207. # while feaLib points to an empty array with count 0
  208. # TODO: make them do the same
  209. table.table.LookupList = otTables.LookupList()
  210. table.table.LookupList.Lookup = []
  211. table.table.LookupList.LookupCount = 0
  212. table.table.LookupList.Lookup.append(synthLookup)
  213. table.table.LookupList.LookupCount += 1
  214. if feature.Feature.LookupListIndex[:1] != [synthLookup]:
  215. feature.Feature.LookupListIndex[:0] = [synthLookup]
  216. feature.Feature.LookupCount += 1
  217. DefaultTable.merge(self, m, tables)
  218. return self
  219. @add_method(
  220. otTables.SingleSubst,
  221. otTables.MultipleSubst,
  222. otTables.AlternateSubst,
  223. otTables.LigatureSubst,
  224. otTables.ReverseChainSingleSubst,
  225. otTables.SinglePos,
  226. otTables.PairPos,
  227. otTables.CursivePos,
  228. otTables.MarkBasePos,
  229. otTables.MarkLigPos,
  230. otTables.MarkMarkPos,
  231. )
  232. def mapLookups(self, lookupMap):
  233. pass
  234. # Copied and trimmed down from subset.py
  235. @add_method(
  236. otTables.ContextSubst,
  237. otTables.ChainContextSubst,
  238. otTables.ContextPos,
  239. otTables.ChainContextPos,
  240. )
  241. def __merge_classify_context(self):
  242. class ContextHelper(object):
  243. def __init__(self, klass, Format):
  244. if klass.__name__.endswith("Subst"):
  245. Typ = "Sub"
  246. Type = "Subst"
  247. else:
  248. Typ = "Pos"
  249. Type = "Pos"
  250. if klass.__name__.startswith("Chain"):
  251. Chain = "Chain"
  252. else:
  253. Chain = ""
  254. ChainTyp = Chain + Typ
  255. self.Typ = Typ
  256. self.Type = Type
  257. self.Chain = Chain
  258. self.ChainTyp = ChainTyp
  259. self.LookupRecord = Type + "LookupRecord"
  260. if Format == 1:
  261. self.Rule = ChainTyp + "Rule"
  262. self.RuleSet = ChainTyp + "RuleSet"
  263. elif Format == 2:
  264. self.Rule = ChainTyp + "ClassRule"
  265. self.RuleSet = ChainTyp + "ClassSet"
  266. if self.Format not in [1, 2, 3]:
  267. return None # Don't shoot the messenger; let it go
  268. if not hasattr(self.__class__, "_merge__ContextHelpers"):
  269. self.__class__._merge__ContextHelpers = {}
  270. if self.Format not in self.__class__._merge__ContextHelpers:
  271. helper = ContextHelper(self.__class__, self.Format)
  272. self.__class__._merge__ContextHelpers[self.Format] = helper
  273. return self.__class__._merge__ContextHelpers[self.Format]
  274. @add_method(
  275. otTables.ContextSubst,
  276. otTables.ChainContextSubst,
  277. otTables.ContextPos,
  278. otTables.ChainContextPos,
  279. )
  280. def mapLookups(self, lookupMap):
  281. c = self.__merge_classify_context()
  282. if self.Format in [1, 2]:
  283. for rs in getattr(self, c.RuleSet):
  284. if not rs:
  285. continue
  286. for r in getattr(rs, c.Rule):
  287. if not r:
  288. continue
  289. for ll in getattr(r, c.LookupRecord):
  290. if not ll:
  291. continue
  292. ll.LookupListIndex = lookupMap[ll.LookupListIndex]
  293. elif self.Format == 3:
  294. for ll in getattr(self, c.LookupRecord):
  295. if not ll:
  296. continue
  297. ll.LookupListIndex = lookupMap[ll.LookupListIndex]
  298. else:
  299. assert 0, "unknown format: %s" % self.Format
  300. @add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
  301. def mapLookups(self, lookupMap):
  302. if self.Format == 1:
  303. self.ExtSubTable.mapLookups(lookupMap)
  304. else:
  305. assert 0, "unknown format: %s" % self.Format
  306. @add_method(otTables.Lookup)
  307. def mapLookups(self, lookupMap):
  308. for st in self.SubTable:
  309. if not st:
  310. continue
  311. st.mapLookups(lookupMap)
  312. @add_method(otTables.LookupList)
  313. def mapLookups(self, lookupMap):
  314. for l in self.Lookup:
  315. if not l:
  316. continue
  317. l.mapLookups(lookupMap)
  318. @add_method(otTables.Lookup)
  319. def mapMarkFilteringSets(self, markFilteringSetMap):
  320. if self.LookupFlag & 0x0010:
  321. self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet]
  322. @add_method(otTables.LookupList)
  323. def mapMarkFilteringSets(self, markFilteringSetMap):
  324. for l in self.Lookup:
  325. if not l:
  326. continue
  327. l.mapMarkFilteringSets(markFilteringSetMap)
  328. @add_method(otTables.Feature)
  329. def mapLookups(self, lookupMap):
  330. self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
  331. @add_method(otTables.FeatureList)
  332. def mapLookups(self, lookupMap):
  333. for f in self.FeatureRecord:
  334. if not f or not f.Feature:
  335. continue
  336. f.Feature.mapLookups(lookupMap)
  337. @add_method(otTables.DefaultLangSys, otTables.LangSys)
  338. def mapFeatures(self, featureMap):
  339. self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
  340. if self.ReqFeatureIndex != 65535:
  341. self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
  342. @add_method(otTables.Script)
  343. def mapFeatures(self, featureMap):
  344. if self.DefaultLangSys:
  345. self.DefaultLangSys.mapFeatures(featureMap)
  346. for l in self.LangSysRecord:
  347. if not l or not l.LangSys:
  348. continue
  349. l.LangSys.mapFeatures(featureMap)
  350. @add_method(otTables.ScriptList)
  351. def mapFeatures(self, featureMap):
  352. for s in self.ScriptRecord:
  353. if not s or not s.Script:
  354. continue
  355. s.Script.mapFeatures(featureMap)
  356. def layoutPreMerge(font):
  357. # Map indices to references
  358. GDEF = font.get("GDEF")
  359. GSUB = font.get("GSUB")
  360. GPOS = font.get("GPOS")
  361. for t in [GSUB, GPOS]:
  362. if not t:
  363. continue
  364. if t.table.LookupList:
  365. lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)}
  366. t.table.LookupList.mapLookups(lookupMap)
  367. t.table.FeatureList.mapLookups(lookupMap)
  368. if (
  369. GDEF
  370. and GDEF.table.Version >= 0x00010002
  371. and GDEF.table.MarkGlyphSetsDef
  372. ):
  373. markFilteringSetMap = {
  374. i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)
  375. }
  376. t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
  377. if t.table.FeatureList and t.table.ScriptList:
  378. featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)}
  379. t.table.ScriptList.mapFeatures(featureMap)
  380. # TODO FeatureParams nameIDs
  381. def layoutPostMerge(font):
  382. # Map references back to indices
  383. GDEF = font.get("GDEF")
  384. GSUB = font.get("GSUB")
  385. GPOS = font.get("GPOS")
  386. for t in [GSUB, GPOS]:
  387. if not t:
  388. continue
  389. if t.table.FeatureList and t.table.ScriptList:
  390. # Collect unregistered (new) features.
  391. featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord)
  392. t.table.ScriptList.mapFeatures(featureMap)
  393. # Record used features.
  394. featureMap = AttendanceRecordingIdentityDict(
  395. t.table.FeatureList.FeatureRecord
  396. )
  397. t.table.ScriptList.mapFeatures(featureMap)
  398. usedIndices = featureMap.s
  399. # Remove unused features
  400. t.table.FeatureList.FeatureRecord = [
  401. f
  402. for i, f in enumerate(t.table.FeatureList.FeatureRecord)
  403. if i in usedIndices
  404. ]
  405. # Map back to indices.
  406. featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord)
  407. t.table.ScriptList.mapFeatures(featureMap)
  408. t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord)
  409. if t.table.LookupList:
  410. # Collect unregistered (new) lookups.
  411. lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup)
  412. t.table.FeatureList.mapLookups(lookupMap)
  413. t.table.LookupList.mapLookups(lookupMap)
  414. # Record used lookups.
  415. lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup)
  416. t.table.FeatureList.mapLookups(lookupMap)
  417. t.table.LookupList.mapLookups(lookupMap)
  418. usedIndices = lookupMap.s
  419. # Remove unused lookups
  420. t.table.LookupList.Lookup = [
  421. l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices
  422. ]
  423. # Map back to indices.
  424. lookupMap = NonhashableDict(t.table.LookupList.Lookup)
  425. t.table.FeatureList.mapLookups(lookupMap)
  426. t.table.LookupList.mapLookups(lookupMap)
  427. t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup)
  428. if GDEF and GDEF.table.Version >= 0x00010002:
  429. markFilteringSetMap = NonhashableDict(
  430. GDEF.table.MarkGlyphSetsDef.Coverage
  431. )
  432. t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
  433. # TODO FeatureParams nameIDs