| 1 | inlineGroupInstance = (list, tuple, set) |
|---|
| 2 | |
|---|
| 3 | class KernFeatureWriter(object): |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | This object will create a kerning feature in FDK |
|---|
| 7 | syntax using the kerning in the given font. The |
|---|
| 8 | only external method is :meth:`ufo2fdk.tools.kernFeatureWriter.write`. |
|---|
| 9 | """ |
|---|
| 10 | |
|---|
| 11 | def __init__(self, font): |
|---|
| 12 | self.font = font |
|---|
| 13 | self.leftGroups, self.rightGroups = self.getReferencedGroups() |
|---|
| 14 | self.unreferencedGroups = self.getUnreferencedGroups() |
|---|
| 15 | self.pairs = self.getPairs() |
|---|
| 16 | self.flatLeftGroups, self.flatRightGroups, self.flatUnreferencedGroups = self.getFlatGroups() |
|---|
| 17 | |
|---|
| 18 | def write(self, headerText=None): |
|---|
| 19 | """ |
|---|
| 20 | Write the feature text. If *headerText* is provided |
|---|
| 21 | it will inserted after the ``feature kern {`` line. |
|---|
| 22 | """ |
|---|
| 23 | glyphGlyph, glyphGroupDecomposed, groupGlyphDecomposed, glyphGroup, groupGlyph, groupGroup = self.getSeparatedPairs(self.pairs) |
|---|
| 24 | # write the classes |
|---|
| 25 | groups = dict(self.leftGroups) |
|---|
| 26 | groups.update(self.rightGroups) |
|---|
| 27 | for groupName, glyphList in groups.items(): |
|---|
| 28 | if not glyphList: |
|---|
| 29 | del groups[groupName] |
|---|
| 30 | classes = self.getClassDefinitionsForGroups(groups) |
|---|
| 31 | # write the kerning rules |
|---|
| 32 | rules = [] |
|---|
| 33 | order = [ |
|---|
| 34 | ("# glyph, glyph", glyphGlyph), |
|---|
| 35 | ("# glyph, group exceptions", glyphGroupDecomposed), |
|---|
| 36 | ("# group exceptions, glyph", groupGlyphDecomposed), |
|---|
| 37 | ("# glyph, group", glyphGroup), |
|---|
| 38 | ("# group, glyph", groupGlyph), |
|---|
| 39 | ("# group, group", groupGroup), |
|---|
| 40 | ] |
|---|
| 41 | for note, pairs in order: |
|---|
| 42 | if pairs: |
|---|
| 43 | rules.append("") |
|---|
| 44 | rules.append(note) |
|---|
| 45 | rules += self.getFeatureRulesForPairs(pairs) |
|---|
| 46 | # compile |
|---|
| 47 | feature = ["feature kern {"] |
|---|
| 48 | if headerText: |
|---|
| 49 | for line in headerText.splitlines(): |
|---|
| 50 | line = line.strip() |
|---|
| 51 | if not line.startswith("#"): |
|---|
| 52 | line = "# " + line |
|---|
| 53 | line = " " + line |
|---|
| 54 | feature.append(line) |
|---|
| 55 | for line in classes + rules: |
|---|
| 56 | if line: |
|---|
| 57 | line = " " + line |
|---|
| 58 | feature.append(line) |
|---|
| 59 | feature.append("} kern;") |
|---|
| 60 | # done |
|---|
| 61 | return u"\n".join(feature) |
|---|
| 62 | |
|---|
| 63 | # ------------- |
|---|
| 64 | # Initial Setup |
|---|
| 65 | # ------------- |
|---|
| 66 | |
|---|
| 67 | def getReferencedGroups(self): |
|---|
| 68 | """ |
|---|
| 69 | Get two dictionaries representing groups |
|---|
| 70 | referenced on the left and right of pairs. |
|---|
| 71 | You should not call this method directly. |
|---|
| 72 | """ |
|---|
| 73 | leftReferencedGroups = set() |
|---|
| 74 | rightReferencedGroups = set() |
|---|
| 75 | groups = self.font.groups |
|---|
| 76 | for left, right in self.font.kerning.keys(): |
|---|
| 77 | if left in groups: |
|---|
| 78 | leftReferencedGroups.add(left) |
|---|
| 79 | if right in groups: |
|---|
| 80 | rightReferencedGroups.add(right) |
|---|
| 81 | leftGroups = {} |
|---|
| 82 | for groupName in leftReferencedGroups: |
|---|
| 83 | glyphList = [glyphName for glyphName in groups[groupName] if glyphName in self.font] |
|---|
| 84 | glyphList = set(glyphList) |
|---|
| 85 | if not groupName.startswith("@"): |
|---|
| 86 | groupName = "@" + groupName |
|---|
| 87 | leftGroups[groupName] = glyphList |
|---|
| 88 | rightGroups = {} |
|---|
| 89 | for groupName in rightReferencedGroups: |
|---|
| 90 | glyphList = [glyphName for glyphName in groups[groupName] if glyphName in self.font] |
|---|
| 91 | glyphList = set(glyphList) |
|---|
| 92 | if not groupName.startswith("@"): |
|---|
| 93 | groupName = "@" + groupName |
|---|
| 94 | rightGroups[groupName] = glyphList |
|---|
| 95 | return leftGroups, rightGroups |
|---|
| 96 | |
|---|
| 97 | def getUnreferencedGroups(self): |
|---|
| 98 | """ |
|---|
| 99 | Get a dictionary representing kerning groups |
|---|
| 100 | that are not referenced in any kerning pairs. |
|---|
| 101 | You should not call this method directly. |
|---|
| 102 | """ |
|---|
| 103 | unreferencedGroups = {} |
|---|
| 104 | for groupName, glyphList in self.font.groups.items(): |
|---|
| 105 | if not groupName.startswith("@"): |
|---|
| 106 | continue |
|---|
| 107 | if groupName in self.leftGroups: |
|---|
| 108 | continue |
|---|
| 109 | if groupName in self.rightGroups: |
|---|
| 110 | continue |
|---|
| 111 | unreferencedGroups[groupName] = set(glyphList) |
|---|
| 112 | return unreferencedGroups |
|---|
| 113 | |
|---|
| 114 | def getPairs(self): |
|---|
| 115 | """ |
|---|
| 116 | Get a dictionary containing all kerning pairs. |
|---|
| 117 | This should filter out pairs containing empty groups |
|---|
| 118 | and groups/glyphs that are not in the font. |
|---|
| 119 | You should not call this method directly. |
|---|
| 120 | """ |
|---|
| 121 | pairs = {} |
|---|
| 122 | for (left, right), value in self.font.kerning.items(): |
|---|
| 123 | # skip missing glyphs |
|---|
| 124 | if left not in self.font.groups and left not in self.font: |
|---|
| 125 | continue |
|---|
| 126 | if right not in self.font.groups and right not in self.font: |
|---|
| 127 | continue |
|---|
| 128 | # skip empty groups |
|---|
| 129 | if left in self.font.groups and not self.font.groups[left]: |
|---|
| 130 | continue |
|---|
| 131 | if right in self.font.groups and not self.font.groups[right]: |
|---|
| 132 | continue |
|---|
| 133 | # store pair |
|---|
| 134 | if left in self.font.groups: |
|---|
| 135 | if not left.startswith("@"): |
|---|
| 136 | left = "@" + left |
|---|
| 137 | if right in self.font.groups: |
|---|
| 138 | if not right.startswith("@"): |
|---|
| 139 | right = "@" + right |
|---|
| 140 | pairs[left, right] = value |
|---|
| 141 | return pairs |
|---|
| 142 | |
|---|
| 143 | def getFlatGroups(self): |
|---|
| 144 | """ |
|---|
| 145 | Get three dictionaries keyed by glyph names with |
|---|
| 146 | group names as values for left, right and |
|---|
| 147 | unreferenced groups. You should not call this |
|---|
| 148 | method directly. |
|---|
| 149 | """ |
|---|
| 150 | flatLeftGroups = {} |
|---|
| 151 | flatRightGroups = {} |
|---|
| 152 | for groupName, glyphList in self.leftGroups.items(): |
|---|
| 153 | for glyphName in glyphList: |
|---|
| 154 | # user has glyph in more than one group. |
|---|
| 155 | # this is not allowed. |
|---|
| 156 | if glyphName in flatLeftGroups: |
|---|
| 157 | continue |
|---|
| 158 | flatLeftGroups[glyphName] = groupName |
|---|
| 159 | for groupName, glyphList in self.rightGroups.items(): |
|---|
| 160 | for glyphName in glyphList: |
|---|
| 161 | # user has glyph in more than one group. |
|---|
| 162 | # this is not allowed. |
|---|
| 163 | if glyphName in flatRightGroups: |
|---|
| 164 | continue |
|---|
| 165 | flatRightGroups[glyphName] = groupName |
|---|
| 166 | flatUnreferencedGroups = {} |
|---|
| 167 | for groupName, glyphList in self.unreferencedGroups.items(): |
|---|
| 168 | for glyphName in glyphList: |
|---|
| 169 | flatUnreferencedGroups[glyphName] = groupName |
|---|
| 170 | return flatLeftGroups, flatRightGroups, flatUnreferencedGroups |
|---|
| 171 | |
|---|
| 172 | # ------------ |
|---|
| 173 | # Pair Support |
|---|
| 174 | # ------------ |
|---|
| 175 | |
|---|
| 176 | def isHigherLevelPairPossible(self, (left, right)): |
|---|
| 177 | """ |
|---|
| 178 | Determine if there is a higher level pair possible. |
|---|
| 179 | This doesn't indicate that the pair exists, it simply |
|---|
| 180 | indicates that something higher than (left, right) |
|---|
| 181 | can exist. |
|---|
| 182 | You should not call this method directly. |
|---|
| 183 | """ |
|---|
| 184 | leftInUnreferenced = False |
|---|
| 185 | rightInUnreferenced = False |
|---|
| 186 | if left.startswith("@"): |
|---|
| 187 | leftGroup = left |
|---|
| 188 | leftGlyph = None |
|---|
| 189 | else: |
|---|
| 190 | leftGroup = self.flatLeftGroups.get(left) |
|---|
| 191 | leftGlyph = left |
|---|
| 192 | if leftGroup is None and left in self.flatUnreferencedGroups: |
|---|
| 193 | leftInUnreferenced= True |
|---|
| 194 | if right.startswith("@"): |
|---|
| 195 | rightGroup = right |
|---|
| 196 | rightGlyph = None |
|---|
| 197 | else: |
|---|
| 198 | rightGroup = self.flatRightGroups.get(right) |
|---|
| 199 | rightGlyph = right |
|---|
| 200 | if rightGroup is None and right in self.flatUnreferencedGroups: |
|---|
| 201 | rightInUnreferenced = True |
|---|
| 202 | |
|---|
| 203 | havePotentialHigherLevelPair = False |
|---|
| 204 | if left.startswith("@") and right.startswith("@"): |
|---|
| 205 | pass |
|---|
| 206 | elif left.startswith("@"): |
|---|
| 207 | if rightGroup is not None or rightInUnreferenced: |
|---|
| 208 | if (left, right) in self.pairs: |
|---|
| 209 | havePotentialHigherLevelPair = True |
|---|
| 210 | elif right.startswith("@"): |
|---|
| 211 | if leftGroup is not None or leftInUnreferenced: |
|---|
| 212 | if (left, right) in self.pairs: |
|---|
| 213 | havePotentialHigherLevelPair = True |
|---|
| 214 | else: |
|---|
| 215 | if leftGroup is not None and rightGroup is not None: |
|---|
| 216 | if (leftGlyph, rightGlyph) in self.pairs: |
|---|
| 217 | havePotentialHigherLevelPair = True |
|---|
| 218 | elif (leftGroup, rightGlyph) in self.pairs: |
|---|
| 219 | havePotentialHigherLevelPair = True |
|---|
| 220 | elif (leftGlyph, rightGroup) in self.pairs: |
|---|
| 221 | havePotentialHigherLevelPair = True |
|---|
| 222 | elif leftGroup is not None: |
|---|
| 223 | if (leftGlyph, rightGlyph) in self.pairs: |
|---|
| 224 | havePotentialHigherLevelPair = True |
|---|
| 225 | elif rightGroup is not None: |
|---|
| 226 | if (leftGlyph, rightGlyph) in self.pairs: |
|---|
| 227 | havePotentialHigherLevelPair = True |
|---|
| 228 | return havePotentialHigherLevelPair |
|---|
| 229 | |
|---|
| 230 | def getSeparatedPairs(self, pairs): |
|---|
| 231 | """ |
|---|
| 232 | Organize *pair* into the following groups: |
|---|
| 233 | |
|---|
| 234 | * glyph, glyph |
|---|
| 235 | * glyph, group (decomposed) |
|---|
| 236 | * group, glyph (decomposed) |
|---|
| 237 | * glyph, group |
|---|
| 238 | * group, glyph |
|---|
| 239 | * group, group |
|---|
| 240 | |
|---|
| 241 | You should not call this method directly. |
|---|
| 242 | """ |
|---|
| 243 | ## seperate pairs |
|---|
| 244 | glyphGlyph = {} |
|---|
| 245 | glyphGroup = {} |
|---|
| 246 | glyphGroupDecomposed = {} |
|---|
| 247 | groupGlyph = {} |
|---|
| 248 | groupGlyphDecomposed = {} |
|---|
| 249 | groupGroup = {} |
|---|
| 250 | for (left, right), value in pairs.items(): |
|---|
| 251 | if left.startswith("@") and right.startswith("@"): |
|---|
| 252 | groupGroup[left, right] = value |
|---|
| 253 | elif left.startswith("@"): |
|---|
| 254 | groupGlyph[left, right] = value |
|---|
| 255 | elif right.startswith("@"): |
|---|
| 256 | glyphGroup[left, right] = value |
|---|
| 257 | else: |
|---|
| 258 | glyphGlyph[left, right] = value |
|---|
| 259 | ## handle decomposition |
|---|
| 260 | allGlyphGlyph = set(glyphGlyph.keys()) |
|---|
| 261 | # glyph to group |
|---|
| 262 | for (left, right), value in glyphGroup.items(): |
|---|
| 263 | if self.isHigherLevelPairPossible((left, right)): |
|---|
| 264 | finalRight = tuple([r for r in sorted(self.rightGroups[right]) if (left, r) not in allGlyphGlyph]) |
|---|
| 265 | for r in finalRight: |
|---|
| 266 | allGlyphGlyph.add((left, r)) |
|---|
| 267 | glyphGroupDecomposed[left, finalRight] = value |
|---|
| 268 | del glyphGroup[left, right] |
|---|
| 269 | # group to glyph |
|---|
| 270 | for (left, right), value in groupGlyph.items(): |
|---|
| 271 | if self.isHigherLevelPairPossible((left, right)): |
|---|
| 272 | finalLeft = tuple([l for l in sorted(self.leftGroups[left]) if (l, right) not in glyphGlyph and (l, right) not in allGlyphGlyph]) |
|---|
| 273 | for l in finalLeft: |
|---|
| 274 | allGlyphGlyph.add((l, right)) |
|---|
| 275 | groupGlyphDecomposed[finalLeft, right] = value |
|---|
| 276 | del groupGlyph[left, right] |
|---|
| 277 | ## return the result |
|---|
| 278 | return glyphGlyph, glyphGroupDecomposed, groupGlyphDecomposed, glyphGroup, groupGlyph, groupGroup |
|---|
| 279 | |
|---|
| 280 | # ------------- |
|---|
| 281 | # Write Support |
|---|
| 282 | # ------------- |
|---|
| 283 | |
|---|
| 284 | def getClassDefinitionsForGroups(self, groups): |
|---|
| 285 | """ |
|---|
| 286 | Write class definitions to a list of strings. |
|---|
| 287 | You should not call this method directly. |
|---|
| 288 | """ |
|---|
| 289 | classes = [] |
|---|
| 290 | for groupName in sorted(groups.keys()): |
|---|
| 291 | group = groups[groupName] |
|---|
| 292 | l = "%s = [%s];" % (groupName, " ".join(sorted(group))) |
|---|
| 293 | classes.append(l) |
|---|
| 294 | return classes |
|---|
| 295 | |
|---|
| 296 | def getFeatureRulesForPairs(self, pairs): |
|---|
| 297 | """ |
|---|
| 298 | Write pair rules to a list of strings. |
|---|
| 299 | You should not call this method directly. |
|---|
| 300 | """ |
|---|
| 301 | rules = [] |
|---|
| 302 | for (left, right), value in sorted(pairs.items()): |
|---|
| 303 | if not left or not right: |
|---|
| 304 | continue |
|---|
| 305 | if isinstance(left, inlineGroupInstance) or isinstance(right, inlineGroupInstance): |
|---|
| 306 | line = "enum pos %s %s %d;" |
|---|
| 307 | else: |
|---|
| 308 | line = "pos %s %s %d;" |
|---|
| 309 | if isinstance(left, inlineGroupInstance): |
|---|
| 310 | left = "[%s]" % " ".join(sorted(left)) |
|---|
| 311 | if isinstance(right, inlineGroupInstance): |
|---|
| 312 | right = "[%s]" % " ".join(sorted(right)) |
|---|
| 313 | rules.append(line % (left, right, value)) |
|---|
| 314 | return rules |
|---|
| 315 | |
|---|
| 316 | |
|---|
| 317 | # ---- |
|---|
| 318 | # Test |
|---|
| 319 | # ---- |
|---|
| 320 | |
|---|
| 321 | |
|---|
| 322 | def _test(): |
|---|
| 323 | """ |
|---|
| 324 | >>> from fontTools.agl import AGL2UV |
|---|
| 325 | >>> from defcon import Font |
|---|
| 326 | >>> font = Font() |
|---|
| 327 | >>> for glyphName in AGL2UV: |
|---|
| 328 | ... font.newGlyph(glyphName) |
|---|
| 329 | >>> kerning = { |
|---|
| 330 | ... # various pair types |
|---|
| 331 | ... ("Agrave", "Agrave") : -100, |
|---|
| 332 | ... ("@LEFT_A", "Agrave") : -75, |
|---|
| 333 | ... ("@LEFT_A", "Aacute") : -74, |
|---|
| 334 | ... ("eight", "@RIGHT_B") : -49, |
|---|
| 335 | ... ("@LEFT_A", "@RIGHT_A") : -25, |
|---|
| 336 | ... ("@LEFT_D", "X") : -25, |
|---|
| 337 | ... ("X", "@RIGHT_D") : -25, |
|---|
| 338 | ... # empty groups |
|---|
| 339 | ... ("@LEFT_C", "@RIGHT_C") : 25, |
|---|
| 340 | ... ("C", "@RIGHT_C") : 25, |
|---|
| 341 | ... ("@LEFT_C", "C") : 25, |
|---|
| 342 | ... # nonexistant glyphs |
|---|
| 343 | ... ("NotInFont", "NotInFont") : 25, |
|---|
| 344 | ... # nonexistant groups |
|---|
| 345 | ... ("@LEFT_NotInFont", "@RIGHT_NotInFont") : 25, |
|---|
| 346 | ... } |
|---|
| 347 | >>> groups = { |
|---|
| 348 | ... "@LEFT_A" : ["A", "Aacute", "Agrave"], |
|---|
| 349 | ... "@RIGHT_A" : ["A", "Aacute", "Agrave"], |
|---|
| 350 | ... "@LEFT_B" : ["B", "eight"], |
|---|
| 351 | ... "@RIGHT_B" : ["B", "eight"], |
|---|
| 352 | ... "@LEFT_C" : [], |
|---|
| 353 | ... "@RIGHT_C" : [], |
|---|
| 354 | ... "@LEFT_D" : ["D"], |
|---|
| 355 | ... "@RIGHT_D" : ["D"], |
|---|
| 356 | ... } |
|---|
| 357 | >>> font.groups.update(groups) |
|---|
| 358 | >>> font.kerning.update(kerning) |
|---|
| 359 | |
|---|
| 360 | >>> writer = KernFeatureWriter(font) |
|---|
| 361 | >>> text = writer.write() |
|---|
| 362 | >>> t1 = [line.strip() for line in text.strip().splitlines()] |
|---|
| 363 | >>> t2 = [line.strip() for line in _expectedFeatureText.strip().splitlines()] |
|---|
| 364 | >>> t1 == t2 |
|---|
| 365 | True |
|---|
| 366 | """ |
|---|
| 367 | |
|---|
| 368 | _expectedFeatureText = """ |
|---|
| 369 | feature kern { |
|---|
| 370 | @LEFT_A = [A Aacute Agrave]; |
|---|
| 371 | @LEFT_D = [D]; |
|---|
| 372 | @RIGHT_A = [A Aacute Agrave]; |
|---|
| 373 | @RIGHT_B = [B eight]; |
|---|
| 374 | @RIGHT_D = [D]; |
|---|
| 375 | |
|---|
| 376 | # glyph, glyph |
|---|
| 377 | pos Agrave Agrave -100; |
|---|
| 378 | |
|---|
| 379 | # glyph, group exceptions |
|---|
| 380 | enum pos eight [B eight] -49; |
|---|
| 381 | |
|---|
| 382 | # group exceptions, glyph |
|---|
| 383 | enum pos [A Aacute] Agrave -75; |
|---|
| 384 | enum pos [A Aacute Agrave] Aacute -74; |
|---|
| 385 | |
|---|
| 386 | # glyph, group |
|---|
| 387 | pos X @RIGHT_D -25; |
|---|
| 388 | |
|---|
| 389 | # group, glyph |
|---|
| 390 | pos @LEFT_D X -25; |
|---|
| 391 | |
|---|
| 392 | # group, group |
|---|
| 393 | pos @LEFT_A @RIGHT_A -25; |
|---|
| 394 | } kern; |
|---|
| 395 | """ |
|---|
| 396 | |
|---|
| 397 | if __name__ == "__main__": |
|---|
| 398 | import doctest |
|---|
| 399 | doctest.testmod() |
|---|