1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
import numpy as np
try:
import re2 as re
except ImportError:
import re
from PIL import Image
from pixelterm import xtermcolors
from config import *
default_palette = [
(0x00, 0x00, 0x00), # 0 normal colors
(0xcd, 0x00, 0x00), # 1
(0x00, 0xcd, 0x00), # 2
(0xcd, 0xcd, 0x00), # 3
(0x00, 0x00, 0xee), # 4
(0xcd, 0x00, 0xcd), # 5
(0x00, 0xcd, 0xcd), # 6
(0xe5, 0xe5, 0xe5), # 7
(0x7f, 0x7f, 0x7f), # 8 bright colors
(0xff, 0x00, 0x00), # 9
(0x00, 0xff, 0x00), # 10
(0xff, 0xff, 0x00), # 11
(0x5c, 0x5c, 0xff), # 12
(0xff, 0x00, 0xff), # 13
(0x00, 0xff, 0xff), # 14
(0xff, 0xff, 0xff)] # 15
class CharGenerator:
def __init__(self, seq=None, lg=None, text=''):
settings = False, False, False, default_palette[8], default_palette[0]
if lg:
settings = lg,bold, lg.blink, lg.underscore, lg.fg, lg.bg
self.bold, self.blink, self.underscore, self.fg, self.bg = settings
self.text = text
self.parse_escape_sequence(seq)
def parse_escape_sequence(seq):
codes = list(map(int, seq[2:-1].split(';')))
fg, bg, reverse, i = self.fg, self.bg, False, 0
while i<len(codes):
a = codes[i]
if a in [38, 48]:
if codes[i+1] == 5:
c = xtermcolors.xterm_colors[codes[i+2]]
fg, bg = (c, bg) if a == 38 else (fg, c)
i += 2
elif a == 39:
fg = (0,0,0)
elif a == 49:
bg = (0,0,0)
elif a == 0:
fg, bg = (0,0,0), (0,0,0)
self.bold, self.blink, self.underscore = False, False, False
elif a in range(30, 38):
fg = default_palette[a-30]
elif a in range(90, 98):
fg = default_palette[a-90+8]
elif a in range(40, 48):
bg = default_palette[a-40]
elif a in range(100, 108):
bg = default_palette[a-100+8]
elif a == 7:
reverse = True
elif a == 5:
self.blink = True
elif a == 4:
self.underscore = True
elif a == 1: # Literally "bright", not bold.
self.bold = True
i += 1
fg, bg = bg, fg if reverse else fg, bg
self.fg, self.bg = fg, bg
def generate_char(self, c, now):
fg, bg = self.bg, self.bg if self.blink and now%1.0 < 0.3 else self.fg, self.bg
glyph = font.glyphs_by_codepoint[c]
# Please forgive the string manipulation below.
lookup = {'0': bg, '1': fg}
return [ list(map(lookup.get, FONT_PADDED_BINARY(int(row, 16)))) for row in glyph.get_data() ]
def generate(self, now):
chars = [self.generate_char(c, now) for c in self.text]
# This refers to inter-letter spacing
space = np.zeros((LETTER_SPACING, DISPLAY_HEIGHT, 3))
spaces = [space]*(len(chars)-1)
everything = chars + spaces
everything[::2] = chars
everything[1::2] = spaces
return np.concatenate(everything)
class TextRenderer:
def __init__(self, text, escapes=True):
"""Renders text into a frame buffer
"escapes" tells the renderer whether to interpret escape sequences (True) or not (False).
"""
generators = []
current_generator = CharGenerator()
for esc, char in r'(\x1B\[[0-9;]+m)|(.)'.finditer(text):
if esc:
if current_generator.text != '':
generators.append(current_generator)
current_generator = CharGenerator(esc, current_generator)
elif char:
current_generator.text += char
self.generators = generators + [current_generator]
def frames(self, start):
now = time.time()
zeros = [np.zeros((DISPLAY_WIDTH, DISPLAY_HEIGHT, 3))]
# Pad the array with one screen's worth of zeros on both sides so the text fully scrolls through.
raw = np.concatenate([zeros]+[g.generate(now) for g in generators]+[zeros])
w,h,_ = raw.size
for i in range(DISPLAY_WIDTH+w, 0, -1):
frame = raw[i:i+DISPLAY_WIDTH, :, :]
yield frame, 1/DEFAULT_SCROLL_SPEED
raw = np.concatenate([zeros]+[g.generate(now) for g in generators]+[zeros])
class ImageRenderer:
def __new__(cls, image_data):
img = Image.open(io.BytesIO(image_data))
self.img = img
def frames(self):
img = self.img
palette = img.getpalette()
last_frame = Image.new("RGB", img.size)
# FIXME set delay to 1/10s if the image is animated, only use DEFAULT_IMAGE_DURATION for static images.
delay = img.info.get('duration', DEFAULT_IMAGE_DURATION*1000.0)/1000.0
for frame in ImageSequence.Iterator(img):
#This works around a known bug in Pillow
#See also: http://stackoverflow.com/questions/4904940/python-converting-gif-frames-to-png
frame.putpalette(palette)
c = frame.convert("RGB")
if img.info['background'] != img.info['transparency']:
last_frame.paste(c, c)
else:
last_frame = c
im = last_frame.copy()
im.thumbnail((DISPLAY_WIDTH, DISPLAY_HEIGHT), Image.NEAREST)
data = np.array(im.getdata(), dtype=np.int8)
data.reshape((DISPLAY_WIDTH, DISPLAY_HEIGHT, 3))
yield data, delay
|