1 # Authors: Sylvain MARIE <sylvain.marie@se.com>
2 # + All contributors to <https://github.com/smarie/python-genbadge>
3 #
4 # License: 3-clause BSD, <https://github.com/smarie/python-genbadge/blob/master/LICENSE>
5 import os
6 import sys
7
8 from PIL import ImageFont
9
10 try:
11 from pathlib import Path
12 except ImportError: # pragma: no cover
13 from pathlib2 import Path # python 2
14
15 try:
16 from typing import Union
17 except ImportError: # pragma: no cover
18 pass
19
20 # Migration from pkg_resources to importlib.resources
21 try:
22 # Python 3.9+
23 from importlib.resources import files, as_file
24 except ImportError: # pragma: no cover
25 try:
26 # Python 3.7-3.8 (importlib_resources backport)
27 from importlib_resources import files, as_file
28 except ImportError: # pragma: no cover
29 files = None
30 as_file = None
31
32
33 COLORS = {
34 'brightgreen': '#4c1',
35 'green': '#97ca00',
36 'yellowgreen': '#a4a61d',
37 'yellow': '#dfb317',
38 'orange': '#fe7d37',
39 'red': '#e05d44',
40 'lightgrey': '#9f9f9f',
41 }
42
43
44 class Badge:
45 """
46 A small utility class for badges
47 """
48 def __init__(self,
49 left_txt, # type: str
50 right_txt, # type: str
51 color, # type: str
52 ):
53 self.left_txt = left_txt
54 self.right_txt = right_txt
55 self.color = color
56
57 def __repr__(self):
58 return "[ %s | %s ] color: %s" % (self.left_txt, self.right_txt, self.color)
59
60 def as_svg(self,
61 use_shields=False # type: bool
62 ):
63 """Return a string containing the SVG representation of this badge
64
65 :param use_shields:
66 :return:
67 """
68 if not use_shields:
69 # generate from our local file template
70 return get_svg_badge(label_txt=self.left_txt, msg_txt=self.right_txt, color=self.color)
71 else:
72 # download from requests
73 import requests
74 # url encode test
75 safe_left_txt = requests.utils.quote(self.left_txt, safe='')
76 safe_right_txt = requests.utils.quote(self.right_txt, safe='')
77 safe_color_txt = requests.utils.quote(self.color, safe='')
78 url = 'https://img.shields.io/badge/%s-%s-%s.svg' % (safe_left_txt, safe_right_txt, safe_color_txt)
79 response = requests.get(url, stream=True)
80 return response.text
81
82 def write_to(self,
-
F821
Undefined name 'TextIO'
83 path_or_stream, # type: Union[TextIO, str, Path]
84 use_shields=False, # type: bool
85 clear_left_txt=False # type: bool
86 ):
87 """Write the SVG representation of this badge to the given file
88
89 :param path_or_stream:
90 :param use_shields:
91 :param clear_left_txt:
92 :return:
93 """
94 # convert to a Path
95 if isinstance(path_or_stream, str):
96 path_or_stream = Path(path_or_stream)
97
98 svg = self.as_svg(use_shields=use_shields)
99 if clear_left_txt:
100 svg = svg.replace(">" + self.left_txt + "<", "><")
101
102 # create parent dirs if needed
103 if isinstance(path_or_stream, Path):
104 path_or_stream.parent.mkdir(parents=True, exist_ok=True)
105
106 # finally write to
107 with open(str(path_or_stream), mode="wb") as f:
108 f.write(svg.encode("utf-8"))
109 else:
110 path_or_stream.write(svg)
111
112
113 def get_svg_badge(
114 label_txt, # type: str
115 msg_txt, # type: str
116 color, # type: str
117 label_color=None
118 ):
119 # type: (...) -> str
120 """
121 Reads the SVG template from the package,
122 fills the various information from args and returns the svg string
123 """
124 all_text = "%s: %s" % (label_txt, msg_txt) if label_txt else ("%s" % msg_txt)
125
126 # Same principle as in shields.io
127 template = get_local_badge_template()
128
129 horiz_padding = 5
130 vertical_margin = 0
131
132 has_logo = False # TODO when a logo is inserted
133 total_logo_width = 0
134
135 has_label = len(label_txt) > 0 or label_color
136 label_color = label_color or '#555'
137 label_margin = total_logo_width + 1
138
139 def process_text(left_margin, content):
140 """From renderText()
141 https://github.com/badges/shields/blob/4415d07e8b5bf794e6675cea052cc644d0c81bb5/badge-maker/lib/badge-renderers.js#L113
142 """
143 text_length = preferred_width_of(content, font_size=11, font_name="Verdana")
144 # todo content = escape_xml(content)
145 shadow_margin = 150 + vertical_margin
146 text_margin = 140 + vertical_margin
147 out_text_length = 10 * text_length
148 x = 10 * (left_margin + 0.5 * text_length + horiz_padding)
149 return x, shadow_margin, text_margin, text_length, out_text_length
150
151 label_x, label_shadow_margin, label_text_margin, label_width, label_text_length = \
152 process_text(label_margin, content=label_txt)
153
154 left_width = (label_width + 2 * horiz_padding + total_logo_width) if has_label else 0
155
156 msg_margin = left_width - (1 if len(msg_txt) > 0 else 0)
157 if not has_label:
158 if has_logo:
159 msg_margin = msg_margin + total_logo_width + horiz_padding
160 else:
161 msg_margin = msg_margin + 1
162
163 msg_x, msg_shadow_margin, msg_text_margin, msg_width, msg_text_length = \
164 process_text(msg_margin, content=msg_txt)
165
166 right_width = (msg_width + 2 * horiz_padding)
167 if (has_logo and not has_label):
168 right_width += total_logo_width + horiz_padding - 1
169
170 total_width = left_width + right_width
171
172 to_replace = {
173 "title": all_text,
174 "label_color": get_color(label_color),
175 "color": get_color(color),
176 "total_width": total_width,
177 "left_width": left_width,
178 "right_width": right_width,
179 # label text
180 "left_x": label_x,
181 "left_shadow_margin": label_shadow_margin,
182 "left_text_margin": label_text_margin,
183 "left_out_text_length": label_text_length,
184 "left_text": label_txt,
185 # msg text
186 "right_x": msg_x,
187 "right_shadow_margin": msg_shadow_margin,
188 "right_text_margin": msg_text_margin,
189 "right_out_text_length": msg_text_length,
190 "right_text": msg_txt
191 }
192 for k, v in to_replace.items():
193 template = template.replace("{{ %s }}" % k, str(v))
194
195 return template
196
197
198 def _resource_string(package, resource_name):
199 """Fallback for importlib.resources in older Python versions."""
200 if files and as_file:
201 with as_file(files(package) / resource_name) as f:
202 return f.read_bytes()
203 else:
204 # Fallback to pkg_resources for older Python versions
205 from pkg_resources import resource_string
206
207 return resource_string(package, resource_name)
208
-
E302
Expected 2 blank lines, found 1
209 def _resource_filename(package, resource_name):
210 """Fallback for importlib.resources in older Python versions."""
211 if files and as_file:
212 with as_file(files(package) / resource_name) as f:
213 return str(f)
214 else:
215 # Fallback to pkg_resources for older Python versions
216 from pkg_resources import resource_filename
217
218 return resource_filename(package, resource_name)
219
-
E302
Expected 2 blank lines, found 1
220 def get_local_badge_template():
221 """Reads the SVG file template fgrom the package resources"""
222 template_path = "badge-template.svg"
223 try:
224 template = _resource_string("genbadge", template_path).decode('utf8')
225 except IOError:
226 # error when running on python 2 inside the CliInvoker from click with a change of os.cwd.
227 import genbadge
228 reload(genbadge) # noqa
229 template = _resource_string("genbadge", template_path).decode('utf8')
230
231 return template
232
233
234 def get_color(color_str):
235 try:
236 color_hexa = COLORS[color_str]
237 except KeyError:
238 # assume custom hexa string already
239 color_hexa = color_str
240
241 return color_hexa
242
243
244 def round_up_to_odd(val):
245 return (val + 1) if (val % 2 == 0) else val
246
247
248 def preferred_width_of(txt, font_name, font_size):
249 # Increase chances of pixel grid alignment.
250 font_file = "%s.ttf" % font_name.lower()
251 try:
252 # Try from name only - this works if the font is known by the OS
253 font = ImageFont.truetype(font=font_file, size=font_size)
254 except (IOError if sys.version_info < (3,) else OSError):
255 # Font not found: use the embedded font file from the package
256 font_path = _resource_filename("genbadge", font_file)
257 if not os.path.exists(font_path):
258 # error when running on python 2 inside the CliInvoker from click with a change of os.cwd.
259 import genbadge
260 reload(genbadge) # noqa
261 font_path = _resource_filename("genbadge", font_file)
262
263 font = ImageFont.truetype(font=font_path, size=font_size)
264
265 # PLI.FreeTypeFont does not have a getsize() method, however, the FreeTypeFont class is not part of PLI's API.
266 # Thus, we can not use isinstance(font, FreeTypeFont) here.
267 getsize = getattr(font, "getsize", None)
268 if callable(getsize):
269 width = font.getsize(txt)[0]
270 else:
271 width = font.getbbox(txt)[2] # exists for FreeTypeFont in PLI >= v10.0.0
272 return round_up_to_odd(width)