Coverage for /usr/lib/python3/dist-packages/gpiozero/fonts.py: 13%

45 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-02-10 12:38 +0000

1# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins 

2# 

3# Copyright (c) 2021 Dave Jones <dave@waveform.org.uk> 

4# 

5# SPDX-License-Identifier: BSD-3-Clause 

6 

7from __future__ import ( 

8 unicode_literals, 

9 absolute_import, 

10 print_function, 

11 division, 

12) 

13str = type('') 

14 

15import io 

16from collections import Counter 

17try: 

18 from itertools import izip as zip, izip_longest as zip_longest 

19except ImportError: 

20 from itertools import zip_longest 

21 

22 

23def load_segment_font(filename_or_obj, width, height, pins): 

24 """ 

25 A generic function for parsing segment font definition files. 

26 

27 If you're working with "standard" `7-segment`_ or `14-segment`_ displays 

28 you *don't* want this function; see :func:`load_font_7seg` or 

29 :func:`load_font_14seg` instead. However, if you are working with another 

30 style of segmented display and wish to construct a parser for a custom 

31 format, this is the function you want. 

32 

33 The *filename_or_obj* parameter is simply the file-like object or filename 

34 to load. This is typically passed in from the calling function. 

35 

36 The *width* and *height* parameters give the width and height in characters 

37 of each character definition. For example, these are 3 and 3 for 7-segment 

38 displays. Finally, *pins* is a list of tuples that defines the position of 

39 each pin definition in the character array, and the character that marks 

40 that position "active". 

41 

42 For example, for 7-segment displays this function is called as follows:: 

43 

44 load_segment_font(filename_or_obj, width=3, height=3, pins=[ 

45 (1, '_'), (5, '|'), (8, '|'), (7, '_'), 

46 (6, '|'), (3, '|'), (4, '_')]) 

47 

48 This dictates that each character will be defined by a 3x3 character grid 

49 which will be converted into a nine-character string like so: 

50 

51 .. code-block:: text 

52 

53 012 

54 345 ==> '012345678' 

55 678 

56 

57 Position 0 is always assumed to be the character being defined. The *pins* 

58 list then specifies: the first pin is the character at position 1 which 

59 will be "on" when that character is "_". The second pin is the character 

60 at position 5 which will be "on" when that character is "|", and so on. 

61 

62 .. _7-segment: https://en.wikipedia.org/wiki/Seven-segment_display 

63 .. _14-segment: https://en.wikipedia.org/wiki/Fourteen-segment_display 

64 """ 

65 assert 0 < len(pins) <= (width * height) - 1 

66 if isinstance(filename_or_obj, bytes): 

67 filename_or_obj = filename_or_obj.decode('utf-8') 

68 opened = isinstance(filename_or_obj, str) 

69 if opened: 

70 filename_or_obj = io.open(filename_or_obj, 'r') 

71 try: 

72 lines = filename_or_obj.read() 

73 if isinstance(lines, bytes): 

74 lines = lines.decode('utf-8') 

75 lines = lines.splitlines() 

76 finally: 

77 if opened: 

78 filename_or_obj.close() 

79 

80 # Strip out comments and blank lines, but remember the original line 

81 # numbers of each row for error reporting purposes 

82 rows = [ 

83 (index, line) for index, line in enumerate(lines, start=1) 

84 # Strip comments and blank (or whitespace) lines 

85 if line.strip() and not line.startswith('#') 

86 ] 

87 line_numbers = { 

88 row_index: line_index 

89 for row_index, (line_index, row) in enumerate(rows) 

90 } 

91 rows = [row for index, row in rows] 

92 if len(rows) % height: 

93 raise ValueError('number of definition lines is not divisible by ' 

94 '{height}'.format(height=height)) 

95 

96 # Strip out blank columns then transpose back to rows, and make sure 

97 # everything is the right "shape" 

98 for n in range(0, len(rows), height): 

99 cols = [ 

100 col for col in zip_longest(*rows[n:n + height], fillvalue=' ') 

101 # Strip blank (or whitespace) columns 

102 if ''.join(col).strip() 

103 ] 

104 rows[n:n + height] = list(zip(*cols)) 

105 for row_index, row in enumerate(rows): 

106 if len(row) % width: 

107 raise ValueError( 

108 'length of definitions starting on line {line} is not ' 

109 'divisible by {width}'.format( 

110 line=line_numbers[row_index], width=width)) 

111 

112 # Split rows up into character definitions. After this, chars will be a 

113 # list of strings each with width x height characters. The first character 

114 # in each string will be the character being defined 

115 chars = [ 

116 ''.join( 

117 char 

118 for row in rows[y::height] 

119 for char in row 

120 )[x::width] 

121 for y in range(height) 

122 for x in range(width) 

123 ] 

124 chars = [''.join(char) for char in zip(*chars)] 

125 

126 # Strip out blank entries (a consequence of zip_longest above) and check 

127 # there're no repeat definitions 

128 chars = [char for char in chars if char.strip()] 

129 counts = Counter(char[0] for char in chars) 

130 for char, count in counts.most_common(): 

131 if count > 1: 

132 raise ValueError( 

133 'multiple definitions for {char!r}'.format(char=char)) 

134 

135 return { 

136 char[0]: tuple(int(char[pos] == on) for pos, on in pins) 

137 for char in chars 

138 } 

139 

140 

141def load_font_7seg(filename_or_obj): 

142 """ 

143 Given a filename or a file-like object, parse it as an font definition for 

144 a `7-segment display`_, returning a :class:`dict` suitable for use with 

145 :class:`~gpiozero.LEDCharDisplay`. 

146 

147 The file-format is a simple text-based format in which blank and #-prefixed 

148 lines are ignored. All other lines are assumed to be groups of character 

149 definitions which are cells of 3x3 characters laid out as follows: 

150 

151 .. code-block:: text 

152 

153 Ca 

154 fgb 

155 edc 

156 

157 Where C is the character being defined, and a-g define the states of the 

158 LEDs for that position. a, d, and g are on if they are "_". b, c, e, and 

159 f are on if they are "|". Any other character in these positions is 

160 considered off. For example, you might define the following characters: 

161 

162 .. code-block:: text 

163 

164 . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ 

165 ... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_| 

166 ... |_| ..| |_. ._| ..| ._| |_| ..| |_| ._| 

167 

168 In the example above, empty locations are marked with "." but could mostly 

169 be left as spaces. However, the first item defines the space (" ") 

170 character and needs *some* non-space characters in its definition as the 

171 parser also strips empty columns (as typically occur between character 

172 definitions). This is also why the definition for "1" must include 

173 something to fill the middle column. 

174 

175 .. _7-segment display: https://en.wikipedia.org/wiki/Seven-segment_display 

176 """ 

177 return load_segment_font(filename_or_obj, width=3, height=3, pins=[ 

178 (1, '_'), (5, '|'), (8, '|'), (7, '_'), 

179 (6, '|'), (3, '|'), (4, '_')]) 

180 

181 

182def load_font_14seg(filename_or_obj): 

183 """ 

184 Given a filename or a file-like object, parse it as a font definition for a 

185 `14-segment display`_, returning a :class:`dict` suitable for use with 

186 :class:`~gpiozero.LEDCharDisplay`. 

187 

188 The file-format is a simple text-based format in which blank and #-prefixed 

189 lines are ignored. All other lines are assumed to be groups of character 

190 definitions which are cells of 5x5 characters laid out as follows: 

191 

192 .. code-block:: text 

193 

194 X.a.. 

195 fijkb 

196 .g.h. 

197 elmnc 

198 ..d.. 

199 

200 Where X is the character being defined, and a-n define the states of the 

201 LEDs for that position. a, d, g, and h are on if they are "-". b, c, e, f, 

202 j, and m are on if they are "|". i and n are on if they are "\\". Finally, 

203 k and l are on if they are "/". Any other character in these positions is 

204 considered off. For example, you might define the following characters: 

205 

206 .. code-block:: text 

207 

208 .... 0--- 1.. 2--- 3--- 4 5--- 6--- 7---. 8--- 9--- 

209 ..... | /| /| | | | | | | / | | | | 

210 ..... | / | | --- -- ---| --- |--- | --- ---| 

211 ..... |/ | | | | | | | | | | | | 

212 ..... --- --- --- --- --- --- 

213 

214 In the example above, several locations have extraneous characters. For 

215 example, the "/" in the center of the "0" definition, or the "-" in the 

216 middle of the "8". These locations are ignored, but filled in nonetheless 

217 to make the shape more obvious. 

218 

219 These extraneous locations could equally well be left as spaces. However, 

220 the first item defines the space (" ") character and needs *some* non-space 

221 characters in its definition as the parser also strips empty columns (as 

222 typically occur between character definitions) and verifies that 

223 definitions are 5 columns wide and 5 rows high. 

224 

225 This also explains why place-holder characters (".") have been inserted at 

226 the top of the definition of the "1" character. Otherwise the parser will 

227 strip these empty columns and decide the definition is invalid (as the 

228 result is only 3 columns wide). 

229 

230 .. _14-segment display: https://en.wikipedia.org/wiki/Fourteen-segment_display 

231 """ 

232 return load_segment_font(filename_or_obj, width=5, height=5, pins=[ 

233 (2, '-'), (9, '|'), (19, '|'), (22, '-'), 

234 (15, '|'), (5, '|'), (11, '-'), (13, '-'), 

235 (6, '\\'), (7, '|'), (8, '/'), (16, '/'), 

236 (17, '|'), (18, '\\')])