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
« 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
7from __future__ import (
8 unicode_literals,
9 absolute_import,
10 print_function,
11 division,
12)
13str = type('')
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
23def load_segment_font(filename_or_obj, width, height, pins):
24 """
25 A generic function for parsing segment font definition files.
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.
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.
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".
42 For example, for 7-segment displays this function is called as follows::
44 load_segment_font(filename_or_obj, width=3, height=3, pins=[
45 (1, '_'), (5, '|'), (8, '|'), (7, '_'),
46 (6, '|'), (3, '|'), (4, '_')])
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:
51 .. code-block:: text
53 012
54 345 ==> '012345678'
55 678
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.
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()
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))
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))
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)]
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))
135 return {
136 char[0]: tuple(int(char[pos] == on) for pos, on in pins)
137 for char in chars
138 }
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`.
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:
151 .. code-block:: text
153 Ca
154 fgb
155 edc
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:
162 .. code-block:: text
164 . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_
165 ... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_|
166 ... |_| ..| |_. ._| ..| ._| |_| ..| |_| ._|
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.
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, '_')])
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`.
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:
192 .. code-block:: text
194 X.a..
195 fijkb
196 .g.h.
197 elmnc
198 ..d..
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:
206 .. code-block:: text
208 .... 0--- 1.. 2--- 3--- 4 5--- 6--- 7---. 8--- 9---
209 ..... | /| /| | | | | | | / | | | |
210 ..... | / | | --- -- ---| --- |--- | --- ---|
211 ..... |/ | | | | | | | | | | | |
212 ..... --- --- --- --- --- ---
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.
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.
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).
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, '\\')])