kerning.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. from __future__ import annotations
  2. from typing import Optional
  3. from fontTools.annotations import KerningPair, KerningDict, KerningGroups, IntFloat
  4. StrDict = dict[str, str]
  5. def lookupKerningValue(
  6. pair: KerningPair,
  7. kerning: KerningDict,
  8. groups: KerningGroups,
  9. fallback: IntFloat = 0,
  10. glyphToFirstGroup: Optional[StrDict] = None,
  11. glyphToSecondGroup: Optional[StrDict] = None,
  12. ) -> IntFloat:
  13. """Retrieve the kerning value (if any) between a pair of elements.
  14. The elments can be either individual glyphs (by name) or kerning
  15. groups (by name), or any combination of the two.
  16. Args:
  17. pair:
  18. A tuple, in logical order (first, second) with respect
  19. to the reading direction, to query the font for kerning
  20. information on. Each element in the tuple can be either
  21. a glyph name or a kerning group name.
  22. kerning:
  23. A dictionary of kerning pairs.
  24. groups:
  25. A set of kerning groups.
  26. fallback:
  27. The fallback value to return if no kern is found between
  28. the elements in ``pair``. Defaults to 0.
  29. glyphToFirstGroup:
  30. A dictionary mapping glyph names to the first-glyph kerning
  31. groups to which they belong. Defaults to ``None``.
  32. glyphToSecondGroup:
  33. A dictionary mapping glyph names to the second-glyph kerning
  34. groups to which they belong. Defaults to ``None``.
  35. Returns:
  36. The kerning value between the element pair. If no kerning for
  37. the pair is found, the fallback value is returned.
  38. Note: This function expects the ``kerning`` argument to be a flat
  39. dictionary of kerning pairs, not the nested structure used in a
  40. kerning.plist file.
  41. Examples::
  42. >>> groups = {
  43. ... "public.kern1.O" : ["O", "D", "Q"],
  44. ... "public.kern2.E" : ["E", "F"]
  45. ... }
  46. >>> kerning = {
  47. ... ("public.kern1.O", "public.kern2.E") : -100,
  48. ... ("public.kern1.O", "F") : -200,
  49. ... ("D", "F") : -300
  50. ... }
  51. >>> lookupKerningValue(("D", "F"), kerning, groups)
  52. -300
  53. >>> lookupKerningValue(("O", "F"), kerning, groups)
  54. -200
  55. >>> lookupKerningValue(("O", "E"), kerning, groups)
  56. -100
  57. >>> lookupKerningValue(("O", "O"), kerning, groups)
  58. 0
  59. >>> lookupKerningValue(("E", "E"), kerning, groups)
  60. 0
  61. >>> lookupKerningValue(("E", "O"), kerning, groups)
  62. 0
  63. >>> lookupKerningValue(("X", "X"), kerning, groups)
  64. 0
  65. >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"),
  66. ... kerning, groups)
  67. -100
  68. >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups)
  69. -200
  70. >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups)
  71. -100
  72. >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups)
  73. 0
  74. """
  75. # quickly check to see if the pair is in the kerning dictionary
  76. if pair in kerning:
  77. return kerning[pair]
  78. # ensure both or no glyph-to-group mappings are provided
  79. if (glyphToFirstGroup is None) != (glyphToSecondGroup is None):
  80. raise ValueError(
  81. "Must provide both 'glyphToFirstGroup' and 'glyphToSecondGroup', or neither."
  82. )
  83. # create glyph to group mapping
  84. if glyphToFirstGroup is None:
  85. glyphToFirstGroup = {}
  86. glyphToSecondGroup = {}
  87. for group, groupMembers in groups.items():
  88. if group.startswith("public.kern1."):
  89. for glyph in groupMembers:
  90. glyphToFirstGroup[glyph] = group
  91. elif group.startswith("public.kern2."):
  92. for glyph in groupMembers:
  93. glyphToSecondGroup[glyph] = group
  94. # ensure type safety for mappings
  95. assert glyphToFirstGroup is not None
  96. assert glyphToSecondGroup is not None
  97. # get group names and make sure first and second are glyph names
  98. first, second = pair
  99. firstGroup = secondGroup = None
  100. if first.startswith("public.kern1."):
  101. firstGroup = first
  102. firstGlyph = None
  103. else:
  104. firstGroup = glyphToFirstGroup.get(first)
  105. firstGlyph = first
  106. if second.startswith("public.kern2."):
  107. secondGroup = second
  108. secondGlyph = None
  109. else:
  110. secondGroup = glyphToSecondGroup.get(second)
  111. secondGlyph = second
  112. # make an ordered list of pairs to look up
  113. pairs = [
  114. (a, b)
  115. for a in (firstGlyph, firstGroup)
  116. for b in (secondGlyph, secondGroup)
  117. if a is not None and b is not None
  118. ]
  119. # look up the pairs and return any matches
  120. for pair in pairs:
  121. if pair in kerning:
  122. return kerning[pair]
  123. # use the fallback value
  124. return fallback
  125. if __name__ == "__main__":
  126. import doctest
  127. doctest.testmod()