1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
# Copyright 2018 The pybadge Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Creates a JSON file that can be used by precalculated_text_measurer.py.
Creates a JSON file that can be used by
precalculated_test_measurer.PrecalculatedTextMeasurer to calculate the pixel
length of text strings rendered in DejaVu Sans font.
The output JSON object is formatted like:
{
'mean-character-length': <float: the average pixel length of a character in
DejaVu Sans at 110pt>,
'character-lengths': <Map[str, float]: a mapping between single characters
and their length in DejaVu Sans at 110pt>,
'kerning-characters': <str: a string containing all of the characters that
kerning information is available for>,
'kerning-pairs': <Map[str, float]: a mapping between pairs of characters
(e.g. "IJ") to the amount of kerning distance between
them. Positive values are *subtracted* from the string
length e.g. text-length-of("IJ") =>
(character-lengths["I"] + character-lengths["J"]
- kerning-pairs["IJ"])
If two characters both appear in 'kerning-characters' but
don't have an entry in 'kerning-pairs' then the kerning
distance between them is zero.
}
For information about the commands, run:
$ python3 - m pybadges.precalculate_text --help
"""
import argparse
import itertools
import json
import os.path
import statistics
from typing import Iterable, Mapping, TextIO
from fontTools import ttLib
from pybadges import pil_text_measurer
from pybadges import text_measurer
def generate_supported_characters(deja_vu_sans_path: str) -> Iterable[str]:
"""Generate the characters support by the font at the given path."""
font = ttLib.TTFont(deja_vu_sans_path)
for cmap in font['cmap'].tables:
if cmap.isUnicode():
for code in cmap.cmap:
yield chr(code)
def generate_encodeable_characters(characters: Iterable[str],
encodings: Iterable[str]) -> Iterable[str]:
"""Generates the subset of 'characters' that can be encoded by 'encodings'.
Args:
characters: The characters to check for encodeability e.g. 'abcd'.
encodings: The encodings to check against e.g. ['cp1252', 'iso-8859-5'].
Returns:
The subset of 'characters' that can be encoded using one of the provided
encodings.
"""
for c in characters:
for encoding in encodings:
try:
c.encode(encoding)
yield c
except UnicodeEncodeError:
pass
def calculate_character_to_length_mapping(
measurer: text_measurer.TextMeasurer,
characters: Iterable[str]) -> Mapping[str, float]:
"""Return a mapping between each given character and its length.
Args:
measurer: The TextMeasurer used to measure the width of the text in
pixels.
characters: The characters to measure e.g. "ml".
Returns:
A mapping from the given characters to their length in pixels, as
determined by 'measurer' e.g. {'m': 5.2, 'l', 1.2}.
"""
char_to_length = {}
for c in characters:
char_to_length[c] = measurer.text_width(c)
return char_to_length
def calculate_pair_to_kern_mapping(measurer: text_measurer.TextMeasurer,
char_to_length: Mapping[str, float],
characters: str) -> Mapping[str, float]:
"""Returns a mapping between each *pair* of characters and their kerning.
Args:
measurer: The TextMeasurer used to measure the width of each pair of
characters.
char_to_length: A mapping between characters and their length in pixels.
Must contain every character in 'characters' e.g.
{'h': 5.2, 'e': 4.0, 'l', 1.2, 'o': 5.0}.
characters: The characters to generate the kerning mapping for e.g.
'hel'.
Returns:
A mapping between each pair of given characters
(e.g. 'hh', he', hl', 'eh', 'ee', 'el', 'lh, 'le', 'll') and the kerning
adjustment for that pair of characters i.e. the difference between the
length of the two characters calculated using 'char_to_length' vs.
the length calculated by `measurer`. Positive values indicate that the
length is less than using the sum of 'char_to_length'. Zero values are
excluded from the map e.g. {'hl': 3.1, 'ee': -0.5}.
"""
pair_to_kerning = {}
for a, b in itertools.permutations(characters, 2):
kerned_width = measurer.text_width(a + b)
unkerned_width = char_to_length[a] + char_to_length[b]
kerning = unkerned_width - kerned_width
if abs(kerning) > 0.05:
pair_to_kerning[a + b] = round(kerning, 3)
return pair_to_kerning
def write_json(f: TextIO, deja_vu_sans_path: str,
measurer: text_measurer.TextMeasurer,
encodings: Iterable[str]) -> None:
"""Write the data required by PrecalculatedTextMeasurer to a stream."""
supported_characters = list(
generate_supported_characters(deja_vu_sans_path))
kerning_characters = ''.join(
generate_encodeable_characters(supported_characters, encodings))
char_to_length = calculate_character_to_length_mapping(
measurer, supported_characters)
pair_to_kerning = calculate_pair_to_kern_mapping(measurer, char_to_length,
kerning_characters)
json.dump(
{
'mean-character-length': statistics.mean(char_to_length.values()),
'character-lengths': char_to_length,
'kerning-characters': kerning_characters,
'kerning-pairs': pair_to_kerning
},
f,
sort_keys=True,
indent=1)
def main():
parser = argparse.ArgumentParser(
description='generate a github-style badge given some text and colors')
parser.add_argument(
'--deja-vu-sans-path',
required=True,
help='the path to the ttf font file containing DejaVu Sans. If not ' +
'present on your system, you can download it from ' +
'https://www.fontsquirrel.com/fonts/dejavu-sans')
parser.add_argument(
'--kerning-pair-encodings',
action='append',
default=['cp1252'],
help='only include kerning pairs for the given encodings')
parser.add_argument(
'--output-json-file',
default=os.path.join(os.path.dirname(__file__), 'default-widths.json'),
help='the path where the generated JSON will be placed. If the ' +
'provided filename extension ends with .xz then the output' +
'will be compressed using lzma.')
args = parser.parse_args()
measurer = pil_text_measurer.PilMeasurer(args.deja_vu_sans_path)
def create_file():
if args.output_json_file.endswith('.xz'):
import lzma
return lzma.open(args.output_json_file, 'wt')
else:
return open(args.output_json_file, 'wt')
with create_file() as f:
write_json(f, args.deja_vu_sans_path, measurer,
args.kerning_pair_encodings)
if __name__ == '__main__':
main()
|