⬅ genbadge/utils_badge.py source

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