PageRenderTime 19ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/scala/net/maniacchef/sfntly/layout/gsub.scala

https://github.com/behnam/sfntly-layout
Scala | 459 lines | 357 code | 75 blank | 27 comment | 8 complexity | ba41928f021ace3669b6d9baf139aa08 MD5 | raw file
  1. package net.maniacchef.sfntly.layout
  2. import java.io.FileInputStream
  3. import java.io.FileOutputStream
  4. import java.io.BufferedInputStream
  5. import scala.collection.mutable.ArrayBuffer
  6. import scala.collection.mutable.ListBuffer
  7. import com.google.typography.font.sfntly.FontFactory
  8. import com.google.typography.font.sfntly.Font
  9. import com.google.typography.font.sfntly.Tag
  10. import com.google.typography.font.sfntly.data.WritableFontData
  11. import com.google.typography.font.sfntly.table.Header
  12. import com.google.typography.font.sfntly.table.Table
  13. import com.google.typography.font.sfntly.table.core.CMap
  14. import com.google.typography.font.sfntly.table.core.CMapTable
  15. // Subtable base interface for GSUB/GPOS
  16. abstract class SubTable(val tableType:Int)
  17. // Layout common tables
  18. case class LangSysTable(
  19. langSysTag:String,
  20. featureList:List[Int]
  21. )
  22. case class ScriptTable(
  23. scriptTag:String,
  24. defaultLangSys:LangSysTable, // DFLT
  25. langSysList:List[LangSysTable]
  26. )
  27. case class FeatureTable(
  28. featureTag:String,
  29. lookupList:List[Int]
  30. )
  31. case class LookupTable(
  32. lookupType:Int,
  33. subtableList:List[SubTable]
  34. ) {
  35. val lookupFlag = 0 // Fixed for now
  36. }
  37. abstract class CoverageTable(val format:Int)
  38. case class CoverageTableFormat1(glyphArray:List[Int]) extends CoverageTable(1)
  39. // Subtable type 1
  40. abstract class SingleSubst(val format:Int) extends SubTable(1)
  41. case class SingleSubstFormat2(
  42. coverage:CoverageTable,
  43. substitutes:List[Int]
  44. ) extends SingleSubst(2)
  45. // Subtable type 2
  46. abstract class MultipleSubst(val format:Int) extends SubTable(2)
  47. case class MultipleSubstFormat1(
  48. coverage:CoverageTable,
  49. sequences:List[List[Int]]
  50. ) extends MultipleSubst(1)
  51. // Subtable type 4 TODO(bashi): Implement
  52. // Subtable type 6
  53. case class SubstLookup(sequenceIndex:Int, lookupIndex:Int)
  54. abstract class ChainingContext(val format:Int) extends SubTable(6)
  55. case class ChainingContextFormat3(
  56. backtrackList:List[CoverageTable],
  57. inputList:List[CoverageTable],
  58. lookaheadList:List[CoverageTable],
  59. substLookupList:List[SubstLookup]
  60. ) extends ChainingContext(3)
  61. // GSUB table
  62. case class GsubTable(
  63. scripts:List[ScriptTable],
  64. features:List[FeatureTable],
  65. lookups:List[LookupTable]
  66. )
  67. // Builder
  68. class GsubTableBuilder(val font:Font) {
  69. var lookups = ArrayBuffer[LookupTable]()
  70. var lookupIndexMap = Map[(List[Int],List[Int]), Int]()
  71. var featureListMap = Map[String, ListBuffer[Int]]()
  72. // Assumes the font has UCS-4 or UCS-2 cmap
  73. private def getCMap():CMap = {
  74. val cmapTable:CMapTable = font.getTable(Tag.cmap)
  75. (cmapTable.cmap(Font.PlatformId.Windows.value(), Font.WindowsEncodingId.UnicodeUCS4.value()),
  76. cmapTable.cmap(Font.PlatformId.Windows.value(), Font.WindowsEncodingId.UnicodeUCS2.value()))
  77. match {
  78. case (ucs4, ucs2) if (ucs4 != null) => ucs4
  79. case (_, ucs2) => ucs2
  80. }
  81. }
  82. private def glyphId(ch:Char) = getCMap().glyphId(ch.toInt)
  83. // Returns the index of appended lookup
  84. private def appendLookup(ltype:Int, subtableList:List[SubTable]) = {
  85. lookups += LookupTable(ltype, subtableList)
  86. lookups.length - 1
  87. }
  88. private def singleSubst(g:Int, r:Int) = {
  89. val key = ((List(g), List(r)))
  90. if (!lookupIndexMap.contains(key)) {
  91. val coverage = CoverageTableFormat1(List(g))
  92. val substitutes = List(r)
  93. val singleSubst = SingleSubstFormat2(coverage, substitutes)
  94. val lookupIndex = appendLookup(1, List(singleSubst))
  95. lookupIndexMap += (key -> lookupIndex)
  96. }
  97. lookupIndexMap(key)
  98. }
  99. private def multipleSubst(g:Int, rx:List[Int]) = {
  100. val key = ((List(g), rx))
  101. if (!lookupIndexMap.contains(key)) {
  102. val coverage = CoverageTableFormat1(List(g))
  103. val multipleSubst = MultipleSubstFormat1(coverage, List(rx))
  104. val lookupIndex = appendLookup(2, List(multipleSubst))
  105. lookupIndexMap += (key -> lookupIndex)
  106. }
  107. lookupIndexMap(key)
  108. }
  109. private def ligSubst(gx:List[Int], r:Int) = {
  110. // TODO(bashi):Implement
  111. 0
  112. }
  113. private def createSubstLookupIndexes(gs:List[Int], rs:List[Int]):List[Int] = (gs, rs) match {
  114. case (x :: List(), y :: List()) => singleSubst(x, y) :: List()
  115. case (x :: xss, y :: List()) => ligSubst(gs, y) :: List()
  116. case (x :: List(), y :: yss) => multipleSubst(x, rs) :: List()
  117. case (x :: xss, y :: yss) => singleSubst(x, y) :: createSubstLookupIndexes(xss, yss)
  118. case (List(), List()) => List()
  119. }
  120. def calt(input:String, subst:String) {
  121. val gs = input.map(glyphId).toList
  122. val rs = subst.map(glyphId).toList
  123. val substLookups = createSubstLookupIndexes(gs, rs).zipWithIndex.map {
  124. case (l, i) => SubstLookup(i, l)
  125. }
  126. val inputCoverages = gs.map((g:Int) => CoverageTableFormat1(List(g)))
  127. val spaceCoverage = CoverageTableFormat1(List(glyphId(' ')))
  128. val backtrackCoverages = List(spaceCoverage)
  129. val lookaheadCoverages = List(spaceCoverage)
  130. val chainingContext = ChainingContextFormat3(backtrackCoverages, inputCoverages,
  131. lookaheadCoverages, substLookups)
  132. val lookupIndex = appendLookup(6, List(chainingContext))
  133. if (!featureListMap.contains("calt"))
  134. featureListMap += ("calt" -> ListBuffer[Int]())
  135. featureListMap("calt") += lookupIndex
  136. }
  137. // Build table
  138. def build():GsubTable = {
  139. val features = featureListMap.toList.map {
  140. case (tag, lookups) => FeatureTable(tag, lookups.toList)
  141. }
  142. // Multiple script isn't supported. Install 'latn'.
  143. val langSys = LangSysTable("DFLT", List.range(0, features.length))
  144. val script = ScriptTable("latn", langSys, List())
  145. var table = new GsubTable(List(script), features, lookups.toList)
  146. table
  147. }
  148. }
  149. // Serializer
  150. class GsubTableSerializer(t:GsubTable) {
  151. var gsub = t
  152. var data = WritableFontData.createWritableFontData(0) // growable
  153. var pos:Int = 0
  154. def serialize() = {
  155. pos = 0
  156. advance(data.writeULong(pos, 0x00010000)) // Version
  157. reserve(6) // For offsets
  158. val scriptOffset = scriptList()
  159. val featureOffset = featureList()
  160. val lookupOffset = lookupList()
  161. data.writeUShort(4, scriptOffset)
  162. data.writeUShort(6, featureOffset)
  163. data.writeUShort(8, lookupOffset)
  164. data
  165. }
  166. def advance(writer: => Int) { pos += writer }
  167. def reserve(len:Int) = { val ret = pos; pos += len; ret }
  168. def reserveForOffset() = { reserve(2) }
  169. def scriptList() = {
  170. val startOffset = pos
  171. advance(data.writeUShort(pos, gsub.scripts.length))
  172. // ScriptRecords
  173. val positions = gsub.scripts.map {
  174. s => {
  175. advance(data.writeULong(pos, Tag.intValue(s.scriptTag)))
  176. reserveForOffset()
  177. }
  178. }
  179. // ScriptTables
  180. val offsets = gsub.scripts.map(scriptTable)
  181. // Fill ScriptTable offsets
  182. positions.zip(offsets).foreach {
  183. case (p, o) => data.writeUShort(p, o - startOffset)
  184. }
  185. startOffset
  186. }
  187. def scriptTable(s:ScriptTable) = {
  188. val startOffset = pos
  189. val defaultLangSysPosition = reserveForOffset()
  190. advance(data.writeUShort(pos, s.langSysList.length))
  191. // LangSysRecords
  192. val positions = s.langSysList.map { l => {
  193. advance(data.writeULong(pos, Tag.intValue(l.langSysTag)))
  194. reserveForOffset()
  195. } }
  196. // DefaultLangSys
  197. val defaultLangSysOffset =
  198. if (s.defaultLangSys != null) langSysTable(s.defaultLangSys) - startOffset
  199. else 0
  200. data.writeUShort(defaultLangSysPosition, defaultLangSysOffset)
  201. // LangSysTables
  202. val offsets = s.langSysList.map(langSysTable)
  203. // Fill LangSysTable offsets
  204. positions.zip(offsets).foreach {
  205. case (p, o) => data.writeUShort(p, o - startOffset)
  206. }
  207. startOffset
  208. }
  209. def langSysTable(l:LangSysTable) = {
  210. val startOffset = pos
  211. advance(data.writeUShort(pos, 0)) // LookupOrder
  212. advance(data.writeUShort(pos, 0xffff)) // ReqFeatureIndex
  213. advance(data.writeUShort(pos, l.featureList.length))
  214. l.featureList.foreach { i => advance(data.writeUShort(pos, i)) }
  215. startOffset
  216. }
  217. def featureList() = {
  218. val startOffset = pos
  219. advance(data.writeUShort(pos, gsub.features.length))
  220. // FeatureRecords
  221. val positions = gsub.features.map { f => {
  222. advance(data.writeULong(pos, Tag.intValue(f.featureTag)))
  223. reserveForOffset()
  224. } }
  225. // FeatureTables
  226. val offsets = gsub.features.map(featureTable)
  227. positions.zip(offsets).foreach {
  228. case (p, o) => data.writeUShort(p, o - startOffset)
  229. }
  230. startOffset
  231. }
  232. def featureTable(f:FeatureTable) = {
  233. val startOffset = pos
  234. advance(data.writeUShort(pos, 0)) // FeatureParams
  235. advance(data.writeUShort(pos, f.lookupList.length))
  236. f.lookupList.foreach { i => advance(data.writeUShort(pos, i)) }
  237. startOffset
  238. }
  239. def lookupList() = {
  240. val startOffset = pos
  241. val count = gsub.lookups.length
  242. advance(data.writeUShort(pos, count))
  243. val offsetsStart = reserve(2 * count) // For Lookup offsets
  244. val offsets = gsub.lookups.map(lookupTable)
  245. offsets.zipWithIndex.foreach {
  246. case (o, i) => {
  247. data.writeUShort(2 * i + offsetsStart, o - startOffset)
  248. }
  249. }
  250. startOffset
  251. }
  252. def lookupTable(l:LookupTable) = {
  253. val startOffset = pos
  254. advance(data.writeUShort(pos, l.lookupType))
  255. advance(data.writeUShort(pos, l.lookupFlag))
  256. advance(data.writeUShort(pos, l.subtableList.length))
  257. val offsetsStart = reserve(2 * l.subtableList.length)
  258. val offsets = l.subtableList.map { s => s match {
  259. case single2:SingleSubstFormat2 => singleSubstFormat2(single2)
  260. case multiple1:MultipleSubstFormat1 => multipleSubstFormat1(multiple1)
  261. case chaining3:ChainingContextFormat3 => chainingContextFormat3(chaining3)
  262. case _ => { println("Ugh"); 0 }
  263. } }
  264. offsets.zipWithIndex.foreach {
  265. case (o, i) => data.writeUShort(2 * i + offsetsStart, o - startOffset)
  266. }
  267. startOffset
  268. }
  269. def coverage(c:CoverageTable) = {
  270. val startOffset = pos
  271. c match {
  272. case c1:CoverageTableFormat1 => {
  273. advance(data.writeUShort(pos, c1.format))
  274. advance(data.writeUShort(pos, c1.glyphArray.length))
  275. c1.glyphArray.foreach { g => advance(data.writeUShort(pos, g)) }
  276. }
  277. case _ => Unit
  278. }
  279. startOffset
  280. }
  281. def singleSubstFormat2(s:SingleSubstFormat2) = {
  282. val startOffset = pos
  283. advance(data.writeUShort(pos, s.format))
  284. val coveragePosition = reserveForOffset()
  285. advance(data.writeUShort(pos, s.substitutes.length))
  286. s.substitutes.foreach { g => advance(data.writeUShort(pos, g)) }
  287. val coverageOffset = coverage(s.coverage) - startOffset
  288. data.writeUShort(coveragePosition, coverageOffset)
  289. startOffset
  290. }
  291. def sequenceTable(gs:List[Int]) = {
  292. val startOffset = pos
  293. advance(data.writeUShort(pos, gs.length))
  294. gs.foreach { g => advance(data.writeUShort(pos, g)) }
  295. startOffset
  296. }
  297. def multipleSubstFormat1(m:MultipleSubstFormat1) = {
  298. val startOffset = pos
  299. advance(data.writeUShort(pos, m.format))
  300. val coveragePosition = reserveForOffset()
  301. advance(data.writeUShort(pos, m.sequences.length))
  302. val offsetsStart = reserve(2 * m.sequences.length)
  303. val coverageOffset = coverage(m.coverage) - startOffset
  304. data.writeUShort(coveragePosition, coverageOffset)
  305. // SequenceTables
  306. var offsets = m.sequences.map(sequenceTable)
  307. offsets.zipWithIndex.foreach {
  308. case (o, i) => data.writeUShort(2 * i + offsetsStart, o - startOffset)
  309. }
  310. startOffset
  311. }
  312. def chainingContextFormat3(c:ChainingContextFormat3) = {
  313. val startOffset = pos
  314. advance(data.writeUShort(pos, c.format))
  315. advance(data.writeUShort(pos, c.backtrackList.length))
  316. val backtrackPosition = reserve(2 * c.backtrackList.length)
  317. advance(data.writeUShort(pos, c.inputList.length))
  318. val inputPosition = reserve(2 * c.inputList.length)
  319. advance(data.writeUShort(pos, c.lookaheadList.length))
  320. val lookaheadPosition = reserve(2 * c.lookaheadList.length)
  321. // SubstLookupRecords
  322. advance(data.writeUShort(pos, c.substLookupList.length))
  323. c.substLookupList.foreach { s => {
  324. advance(data.writeUShort(pos, s.sequenceIndex))
  325. advance(data.writeUShort(pos, s.lookupIndex))
  326. } }
  327. // Create coverage tables and fill offsets
  328. val backtrackOffsets = c.backtrackList.map(coverage)
  329. backtrackOffsets.zipWithIndex.foreach {
  330. case (o, i) => data.writeUShort(2 * i + backtrackPosition, o - startOffset)
  331. }
  332. val inputOffsets = c.inputList.map(coverage)
  333. inputOffsets.zipWithIndex.foreach {
  334. case (o, i) => data.writeUShort(2 * i + inputPosition, o - startOffset)
  335. }
  336. val lookaheadOffsets = c.lookaheadList.map(coverage)
  337. lookaheadOffsets.zipWithIndex.foreach {
  338. case (o, i) => data.writeUShort(2 * i + lookaheadPosition, o - startOffset)
  339. }
  340. startOffset
  341. }
  342. }
  343. // APIs
  344. class Gsub(n:String, f:Font) {
  345. private val srcFile = n
  346. private val font = f
  347. private var builder = new GsubTableBuilder(f)
  348. private def getFontBuilder():Font.Builder = {
  349. val binaryStream = new BufferedInputStream(new FileInputStream(srcFile))
  350. val fontBuilders = FontFactory.getInstance().loadFontsForBuilding(binaryStream)
  351. binaryStream.close()
  352. fontBuilders(0)
  353. }
  354. def contextualAlternate(rules: => Any):Gsub = {
  355. rules match {
  356. case (a:String, b:String) => builder.calt(a, b)
  357. case x :: xs => { contextualAlternate(x); contextualAlternate(xs) }
  358. case _ => Unit
  359. }
  360. this
  361. }
  362. def done(outFile:String) {
  363. var serializer = new GsubTableSerializer(builder.build())
  364. val newGsubData = serializer.serialize()
  365. val fontBuilder = getFontBuilder()
  366. val gsubBuilder = fontBuilder.newTableBuilder(Tag.GSUB, newGsubData)
  367. gsubBuilder.data().setCheckSumRanges()
  368. val outFont = fontBuilder.build()
  369. val fileStream = new FileOutputStream(outFile)
  370. FontFactory.getInstance().serializeFont(outFont, fileStream)
  371. fileStream.close()
  372. }
  373. }
  374. object Gsub {
  375. private def loadFont(fontFile:String):Font = {
  376. val binaryStream = new BufferedInputStream(new FileInputStream(fontFile))
  377. val fonts = FontFactory.getInstance().loadFonts(binaryStream)
  378. binaryStream.close()
  379. fonts(0)
  380. }
  381. def apply(srcFile:String) = {
  382. new Gsub(srcFile, loadFont(srcFile))
  383. }
  384. }