Coverage for genbadge/utils_badge.py: 84%

103 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-11-10 20:37 +0000

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> 

5import os 

6import sys 

7 

8from PIL import ImageFont 

9 

10try: 

11 from pathlib import Path 

12except ImportError: # pragma: no cover 

13 from pathlib2 import Path # python 2 

14 

15try: 

16 from typing import Union 

17except ImportError: # pragma: no cover 

18 pass 

19 

20from pkg_resources import resource_string, resource_filename 

21 

22 

23COLORS = { 

24 'brightgreen': '#4c1', 

25 'green': '#97ca00', 

26 'yellowgreen': '#a4a61d', 

27 'yellow': '#dfb317', 

28 'orange': '#fe7d37', 

29 'red': '#e05d44', 

30 'lightgrey': '#9f9f9f', 

31} 

32 

33 

34class 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='') 

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, 

73 path_or_stream, # type: Union[TextIO, str, Path] 

74 use_shields=False, # type: bool 

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: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true

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 

103def 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: 147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true

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): 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

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 

188def 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 

202def 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 

212def round_up_to_odd(val): 

213 return (val + 1) if (val % 2 == 0) else val 

214 

215 

216def 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): 225 ↛ 227line 225 didn't jump to line 227, because the condition on line 225 was never true

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): 236 ↛ 239line 236 didn't jump to line 239, because the condition on line 236 was never false

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)