layout.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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 = (
  142. otTables.GSUB.mergeMap
  143. ) = (
  144. otTables.GPOS.mergeMap
  145. ) = otTables.BASE.mergeMap = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = {
  146. "*": mergeObjects,
  147. "Version": max,
  148. }
  149. ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass(
  150. "GSUB"
  151. ).mergeMap = ttLib.getTableClass("GPOS").mergeMap = ttLib.getTableClass(
  152. "BASE"
  153. ).mergeMap = ttLib.getTableClass(
  154. "JSTF"
  155. ).mergeMap = ttLib.getTableClass(
  156. "MATH"
  157. ).mergeMap = {
  158. "tableTag": onlyExisting(equal), # XXX clean me up
  159. "table": mergeObjects,
  160. }
  161. @add_method(ttLib.getTableClass("GSUB"))
  162. def merge(self, m, tables):
  163. assert len(tables) == len(m.duplicateGlyphsPerFont)
  164. for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
  165. if not dups:
  166. continue
  167. if table is None or table is NotImplemented:
  168. log.warning(
  169. "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s",
  170. m.fonts[i]._merger__name,
  171. dups,
  172. )
  173. continue
  174. synthFeature = None
  175. synthLookup = None
  176. for script in table.table.ScriptList.ScriptRecord:
  177. if script.ScriptTag == "DFLT":
  178. continue # XXX
  179. for langsys in [script.Script.DefaultLangSys] + [
  180. l.LangSys for l in script.Script.LangSysRecord
  181. ]:
  182. if langsys is None:
  183. continue # XXX Create!
  184. feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"]
  185. assert len(feature) <= 1
  186. if feature:
  187. feature = feature[0]
  188. else:
  189. if not synthFeature:
  190. synthFeature = otTables.FeatureRecord()
  191. synthFeature.FeatureTag = "locl"
  192. f = synthFeature.Feature = otTables.Feature()
  193. f.FeatureParams = None
  194. f.LookupCount = 0
  195. f.LookupListIndex = []
  196. table.table.FeatureList.FeatureRecord.append(synthFeature)
  197. table.table.FeatureList.FeatureCount += 1
  198. feature = synthFeature
  199. langsys.FeatureIndex.append(feature)
  200. langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag)
  201. if not synthLookup:
  202. subtable = otTables.SingleSubst()
  203. subtable.mapping = dups
  204. synthLookup = otTables.Lookup()
  205. synthLookup.LookupFlag = 0
  206. synthLookup.LookupType = 1
  207. synthLookup.SubTableCount = 1
  208. synthLookup.SubTable = [subtable]
  209. if table.table.LookupList is None:
  210. # mtiLib uses None as default value for LookupList,
  211. # while feaLib points to an empty array with count 0
  212. # TODO: make them do the same
  213. table.table.LookupList = otTables.LookupList()
  214. table.table.LookupList.Lookup = []
  215. table.table.LookupList.LookupCount = 0
  216. table.table.LookupList.Lookup.append(synthLookup)
  217. table.table.LookupList.LookupCount += 1
  218. if feature.Feature.LookupListIndex[:1] != [synthLookup]:
  219. feature.Feature.LookupListIndex[:0] = [synthLookup]
  220. feature.Feature.LookupCount += 1
  221. DefaultTable.merge(self, m, tables)
  222. return self
  223. @add_method(
  224. otTables.SingleSubst,
  225. otTables.MultipleSubst,
  226. otTables.AlternateSubst,
  227. otTables.LigatureSubst,
  228. otTables.ReverseChainSingleSubst,
  229. otTables.SinglePos,
  230. otTables.PairPos,
  231. otTables.CursivePos,
  232. otTables.MarkBasePos,
  233. otTables.MarkLigPos,
  234. otTables.MarkMarkPos,
  235. )
  236. def mapLookups(self, lookupMap):
  237. pass
  238. # Copied and trimmed down from subset.py
  239. @add_method(
  240. otTables.ContextSubst,
  241. otTables.ChainContextSubst,
  242. otTables.ContextPos,
  243. otTables.ChainContextPos,
  244. )
  245. def __merge_classify_context(self):
  246. class ContextHelper(object):
  247. def __init__(self, klass, Format):
  248. if klass.__name__.endswith("Subst"):
  249. Typ = "Sub"
  250. Type = "Subst"
  251. else:
  252. Typ = "Pos"
  253. Type = "Pos"
  254. if klass.__name__.startswith("Chain"):
  255. Chain = "Chain"
  256. else:
  257. Chain = ""
  258. ChainTyp = Chain + Typ
  259. self.Typ = Typ
  260. self.Type = Type
  261. self.Chain = Chain
  262. self.ChainTyp = ChainTyp
  263. self.LookupRecord = Type + "LookupRecord"
  264. if Format == 1:
  265. self.Rule = ChainTyp + "Rule"
  266. self.RuleSet = ChainTyp + "RuleSet"
  267. elif Format == 2:
  268. self.Rule = ChainTyp + "ClassRule"
  269. self.RuleSet = ChainTyp + "ClassSet"
  270. if self.Format not in [1, 2, 3]:
  271. return None # Don't shoot the messenger; let it go
  272. if not hasattr(self.__class__, "_merge__ContextHelpers"):
  273. self.__class__._merge__ContextHelpers = {}
  274. if self.Format not in self.__class__._merge__ContextHelpers:
  275. helper = ContextHelper(self.__class__, self.Format)
  276. self.__class__._merge__ContextHelpers[self.Format] = helper
  277. return self.__class__._merge__ContextHelpers[self.Format]
  278. @add_method(
  279. otTables.ContextSubst,
  280. otTables.ChainContextSubst,
  281. otTables.ContextPos,
  282. otTables.ChainContextPos,
  283. )
  284. def mapLookups(self, lookupMap):
  285. c = self.__merge_classify_context()
  286. if self.Format in [1, 2]:
  287. for rs in getattr(self, c.RuleSet):
  288. if not rs:
  289. continue
  290. for r in getattr(rs, c.Rule):
  291. if not r:
  292. continue
  293. for ll in getattr(r, c.LookupRecord):
  294. if not ll:
  295. continue
  296. ll.LookupListIndex = lookupMap[ll.LookupListIndex]
  297. elif self.Format == 3:
  298. for ll in getattr(self, c.LookupRecord):
  299. if not ll:
  300. continue
  301. ll.LookupListIndex = lookupMap[ll.LookupListIndex]
  302. else:
  303. assert 0, "unknown format: %s" % self.Format
  304. @add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
  305. def mapLookups(self, lookupMap):
  306. if self.Format == 1:
  307. self.ExtSubTable.mapLookups(lookupMap)
  308. else:
  309. assert 0, "unknown format: %s" % self.Format
  310. @add_method(otTables.Lookup)
  311. def mapLookups(self, lookupMap):
  312. for st in self.SubTable:
  313. if not st:
  314. continue
  315. st.mapLookups(lookupMap)
  316. @add_method(otTables.LookupList)
  317. def mapLookups(self, lookupMap):
  318. for l in self.Lookup:
  319. if not l:
  320. continue
  321. l.mapLookups(lookupMap)
  322. @add_method(otTables.Lookup)
  323. def mapMarkFilteringSets(self, markFilteringSetMap):
  324. if self.LookupFlag & 0x0010:
  325. self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet]
  326. @add_method(otTables.LookupList)
  327. def mapMarkFilteringSets(self, markFilteringSetMap):
  328. for l in self.Lookup:
  329. if not l:
  330. continue
  331. l.mapMarkFilteringSets(markFilteringSetMap)
  332. @add_method(otTables.Feature)
  333. def mapLookups(self, lookupMap):
  334. self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
  335. @add_method(otTables.FeatureList)
  336. def mapLookups(self, lookupMap):
  337. for f in self.FeatureRecord:
  338. if not f or not f.Feature:
  339. continue
  340. f.Feature.mapLookups(lookupMap)
  341. @add_method(otTables.DefaultLangSys, otTables.LangSys)
  342. def mapFeatures(self, featureMap):
  343. self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
  344. if self.ReqFeatureIndex != 65535:
  345. self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
  346. @add_method(otTables.Script)
  347. def mapFeatures(self, featureMap):
  348. if self.DefaultLangSys:
  349. self.DefaultLangSys.mapFeatures(featureMap)
  350. for l in self.LangSysRecord:
  351. if not l or not l.LangSys:
  352. continue
  353. l.LangSys.mapFeatures(featureMap)
  354. @add_method(otTables.ScriptList)
  355. def mapFeatures(self, featureMap):
  356. for s in self.ScriptRecord:
  357. if not s or not s.Script:
  358. continue
  359. s.Script.mapFeatures(featureMap)
  360. def layoutPreMerge(font):
  361. # Map indices to references
  362. GDEF = font.get("GDEF")
  363. GSUB = font.get("GSUB")
  364. GPOS = font.get("GPOS")
  365. for t in [GSUB, GPOS]:
  366. if not t:
  367. continue
  368. if t.table.LookupList:
  369. lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)}
  370. t.table.LookupList.mapLookups(lookupMap)
  371. t.table.FeatureList.mapLookups(lookupMap)
  372. if (
  373. GDEF
  374. and GDEF.table.Version >= 0x00010002
  375. and GDEF.table.MarkGlyphSetsDef
  376. ):
  377. markFilteringSetMap = {
  378. i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)
  379. }
  380. t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
  381. if t.table.FeatureList and t.table.ScriptList:
  382. featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)}
  383. t.table.ScriptList.mapFeatures(featureMap)
  384. # TODO FeatureParams nameIDs
  385. def layoutPostMerge(font):
  386. # Map references back to indices
  387. GDEF = font.get("GDEF")
  388. GSUB = font.get("GSUB")
  389. GPOS = font.get("GPOS")
  390. for t in [GSUB, GPOS]:
  391. if not t:
  392. continue
  393. if t.table.FeatureList and t.table.ScriptList:
  394. # Collect unregistered (new) features.
  395. featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord)
  396. t.table.ScriptList.mapFeatures(featureMap)
  397. # Record used features.
  398. featureMap = AttendanceRecordingIdentityDict(
  399. t.table.FeatureList.FeatureRecord
  400. )
  401. t.table.ScriptList.mapFeatures(featureMap)
  402. usedIndices = featureMap.s
  403. # Remove unused features
  404. t.table.FeatureList.FeatureRecord = [
  405. f
  406. for i, f in enumerate(t.table.FeatureList.FeatureRecord)
  407. if i in usedIndices
  408. ]
  409. # Map back to indices.
  410. featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord)
  411. t.table.ScriptList.mapFeatures(featureMap)
  412. t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord)
  413. if t.table.LookupList:
  414. # Collect unregistered (new) lookups.
  415. lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup)
  416. t.table.FeatureList.mapLookups(lookupMap)
  417. t.table.LookupList.mapLookups(lookupMap)
  418. # Record used lookups.
  419. lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup)
  420. t.table.FeatureList.mapLookups(lookupMap)
  421. t.table.LookupList.mapLookups(lookupMap)
  422. usedIndices = lookupMap.s
  423. # Remove unused lookups
  424. t.table.LookupList.Lookup = [
  425. l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices
  426. ]
  427. # Map back to indices.
  428. lookupMap = NonhashableDict(t.table.LookupList.Lookup)
  429. t.table.FeatureList.mapLookups(lookupMap)
  430. t.table.LookupList.mapLookups(lookupMap)
  431. t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup)
  432. if GDEF and GDEF.table.Version >= 0x00010002:
  433. markFilteringSetMap = NonhashableDict(
  434. GDEF.table.MarkGlyphSetsDef.Coverage
  435. )
  436. t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
  437. # TODO FeatureParams nameIDs