ttyengine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ttyengine/__init__.py +45 -0
- ttyengine/ansi.py +201 -0
- ttyengine/ascii_styler.py +473 -0
- ttyengine/components.py +4718 -0
- ttyengine/compositor.py +780 -0
- ttyengine/errors.py +181 -0
- ttyengine/figlet-LICENSE.txt +27 -0
- ttyengine/figlet.py +430 -0
- ttyengine/fonts.zip +0 -0
- ttyengine/image_reader.py +813 -0
- ttyengine/input_handler.py +814 -0
- ttyengine/launcher.py +822 -0
- ttyengine/region.py +180 -0
- ttyengine/scene.py +614 -0
- ttyengine/sound.py +1439 -0
- ttyengine/terminal.py +283 -0
- ttyengine-0.1.0.dist-info/METADATA +903 -0
- ttyengine-0.1.0.dist-info/RECORD +21 -0
- ttyengine-0.1.0.dist-info/WHEEL +5 -0
- ttyengine-0.1.0.dist-info/licenses/LICENSE +373 -0
- ttyengine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import os, re, struct, subprocess, zlib
|
|
2
|
+
from array import array
|
|
3
|
+
from math import cos, pi, sqrt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImageError(ValueError):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read(path):
|
|
11
|
+
path = os.fspath(path)
|
|
12
|
+
with open(path, 'rb') as f:
|
|
13
|
+
data = f.read()
|
|
14
|
+
return decode(data, path)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def decode(data, path=None):
|
|
18
|
+
error = None
|
|
19
|
+
for check, decoder in _FORMATS:
|
|
20
|
+
if check(data):
|
|
21
|
+
try:
|
|
22
|
+
return decoder(data)
|
|
23
|
+
except ImageError as e:
|
|
24
|
+
error = e
|
|
25
|
+
except (ArithmeticError, LookupError, MemoryError, TypeError, ValueError, StopIteration, struct.error, zlib.error) as e:
|
|
26
|
+
error = ImageError('corrupt or truncated image data')
|
|
27
|
+
error.__cause__ = e
|
|
28
|
+
break
|
|
29
|
+
converted, reason = _ffmpeg(data, path)
|
|
30
|
+
if converted is not None:
|
|
31
|
+
return converted
|
|
32
|
+
if error is not None:
|
|
33
|
+
raise error
|
|
34
|
+
raise ImageError('unsupported image format (' + reason + ')')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ffmpeg(data, path):
|
|
38
|
+
from .sound import find_ffmpeg
|
|
39
|
+
exe = find_ffmpeg()
|
|
40
|
+
if not exe:
|
|
41
|
+
return None, 'PNG, JPEG, GIF, BMP and PNM are built in; install ffmpeg to read other formats'
|
|
42
|
+
command = [exe, '-v', 'error', '-nostdin', '-i', '{}', '-an', '-sn', '-frames:v', '1', '-c:v', 'png',
|
|
43
|
+
'-pix_fmt', 'rgba', '-pred', 'none', '-f', 'image2pipe', 'pipe:1']
|
|
44
|
+
sources = [('pipe:0', {'input': data})]
|
|
45
|
+
if path is not None:
|
|
46
|
+
sources.append(('file:' + os.path.abspath(path), {'stdin': subprocess.DEVNULL}))
|
|
47
|
+
reason = 'ffmpeg could not decode the image'
|
|
48
|
+
for source, options in sources:
|
|
49
|
+
args = [source if part == '{}' else part for part in command]
|
|
50
|
+
try:
|
|
51
|
+
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120, **options)
|
|
52
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
53
|
+
reason = f'ffmpeg failed: {e}'
|
|
54
|
+
continue
|
|
55
|
+
if proc.returncode == 0 and _is_png(proc.stdout):
|
|
56
|
+
try:
|
|
57
|
+
return _png(proc.stdout), ''
|
|
58
|
+
except (ImageError, ArithmeticError, LookupError, MemoryError, TypeError, ValueError, struct.error, zlib.error):
|
|
59
|
+
reason = 'ffmpeg produced unreadable output'
|
|
60
|
+
continue
|
|
61
|
+
lines = proc.stderr.decode('utf-8', 'replace').strip().splitlines()
|
|
62
|
+
if lines:
|
|
63
|
+
reason = 'ffmpeg: ' + lines[-1]
|
|
64
|
+
return None, reason
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _blank(w, h):
|
|
68
|
+
if w < 1 or h < 1 or w * h > 1 << 28:
|
|
69
|
+
raise ImageError('invalid image dimensions')
|
|
70
|
+
return bytearray(w * h * 4)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_ADAM7 = ((0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4), (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2))
|
|
74
|
+
_PNG_CHANNELS = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_png(d):
|
|
78
|
+
return d.startswith(b'\x89PNG\r\n\x1a\n')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _png_unfilter(raw, pos, stride, bpp, rows):
|
|
82
|
+
prev = bytearray(stride)
|
|
83
|
+
out = []
|
|
84
|
+
for _ in range(rows):
|
|
85
|
+
ft = raw[pos]
|
|
86
|
+
line = bytearray(raw[pos + 1:pos + 1 + stride])
|
|
87
|
+
if len(line) != stride:
|
|
88
|
+
raise ImageError('truncated PNG data')
|
|
89
|
+
pos += 1 + stride
|
|
90
|
+
if ft == 1:
|
|
91
|
+
for i in range(bpp, stride):
|
|
92
|
+
line[i] = (line[i] + line[i - bpp]) & 255
|
|
93
|
+
elif ft == 2:
|
|
94
|
+
for i in range(stride):
|
|
95
|
+
line[i] = (line[i] + prev[i]) & 255
|
|
96
|
+
elif ft == 3:
|
|
97
|
+
for i in range(stride):
|
|
98
|
+
left = line[i - bpp] if i >= bpp else 0
|
|
99
|
+
line[i] = (line[i] + ((left + prev[i]) >> 1)) & 255
|
|
100
|
+
elif ft == 4:
|
|
101
|
+
for i in range(stride):
|
|
102
|
+
a = line[i - bpp] if i >= bpp else 0
|
|
103
|
+
b = prev[i]
|
|
104
|
+
c = prev[i - bpp] if i >= bpp else 0
|
|
105
|
+
p = a + b - c
|
|
106
|
+
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
|
|
107
|
+
pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c)
|
|
108
|
+
line[i] = (line[i] + pr) & 255
|
|
109
|
+
elif ft != 0:
|
|
110
|
+
raise ImageError('invalid PNG filter')
|
|
111
|
+
out.append(line)
|
|
112
|
+
prev = line
|
|
113
|
+
return out, pos
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _png_samples(line, count, depth):
|
|
117
|
+
if depth == 8:
|
|
118
|
+
return line[:count]
|
|
119
|
+
if depth == 16:
|
|
120
|
+
return line[0:count * 2:2]
|
|
121
|
+
per = 8 // depth
|
|
122
|
+
mask = (1 << depth) - 1
|
|
123
|
+
return [(line[i // per] >> (8 - depth * (i % per + 1))) & mask for i in range(count)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _png(data):
|
|
127
|
+
pos = 8
|
|
128
|
+
idat = []
|
|
129
|
+
plte = b''
|
|
130
|
+
trns = b''
|
|
131
|
+
hdr = None
|
|
132
|
+
while pos + 8 <= len(data):
|
|
133
|
+
n, kind = struct.unpack('>I4s', data[pos:pos + 8])
|
|
134
|
+
body = data[pos + 8:pos + 8 + n]
|
|
135
|
+
pos += 12 + n
|
|
136
|
+
if kind == b'IHDR':
|
|
137
|
+
hdr = struct.unpack('>IIBBBBB', body)
|
|
138
|
+
elif kind == b'PLTE':
|
|
139
|
+
plte = body
|
|
140
|
+
elif kind == b'tRNS':
|
|
141
|
+
trns = body
|
|
142
|
+
elif kind == b'IDAT':
|
|
143
|
+
idat.append(body)
|
|
144
|
+
elif kind == b'IEND':
|
|
145
|
+
break
|
|
146
|
+
if hdr is None or not idat:
|
|
147
|
+
raise ImageError('corrupt PNG')
|
|
148
|
+
w, h, depth, ctype, _, _, interlace = hdr
|
|
149
|
+
if ctype not in _PNG_CHANNELS or depth not in (1, 2, 4, 8, 16) or interlace not in (0, 1):
|
|
150
|
+
raise ImageError('invalid PNG header')
|
|
151
|
+
if (ctype == 3 and depth > 8) or (ctype in (2, 4, 6) and depth < 8):
|
|
152
|
+
raise ImageError('invalid PNG header')
|
|
153
|
+
channels = _PNG_CHANNELS[ctype]
|
|
154
|
+
raw = zlib.decompress(b''.join(idat))
|
|
155
|
+
out = _blank(w, h)
|
|
156
|
+
sdepth = 8 if depth == 16 else depth
|
|
157
|
+
maxv = (1 << sdepth) - 1
|
|
158
|
+
keyed = None
|
|
159
|
+
if ctype == 0 and len(trns) >= 2:
|
|
160
|
+
keyed = int.from_bytes(trns[:2], 'big')
|
|
161
|
+
elif ctype == 2 and len(trns) >= 6:
|
|
162
|
+
keyed = tuple(int.from_bytes(trns[i:i + 2], 'big') for i in (0, 2, 4))
|
|
163
|
+
pal = [(plte[i], plte[i + 1], plte[i + 2], trns[i // 3] if i // 3 < len(trns) else 255)
|
|
164
|
+
for i in range(0, len(plte) - 2, 3)]
|
|
165
|
+
bpp = max(1, channels * depth // 8)
|
|
166
|
+
fast = ctype == 6 and depth == 8 and not interlace
|
|
167
|
+
black = (0, 0, 0, 255)
|
|
168
|
+
pos = 0
|
|
169
|
+
for x0, y0, dx, dy in (_ADAM7 if interlace else ((0, 0, 1, 1),)):
|
|
170
|
+
pw = (w - x0 + dx - 1) // dx
|
|
171
|
+
ph = (h - y0 + dy - 1) // dy
|
|
172
|
+
if pw <= 0 or ph <= 0:
|
|
173
|
+
continue
|
|
174
|
+
stride = (pw * channels * depth + 7) // 8
|
|
175
|
+
rows, pos = _png_unfilter(raw, pos, stride, bpp, ph)
|
|
176
|
+
for j, line in enumerate(rows):
|
|
177
|
+
if fast:
|
|
178
|
+
o = j * w * 4
|
|
179
|
+
out[o:o + w * 4] = line
|
|
180
|
+
continue
|
|
181
|
+
s = _png_samples(line, pw * channels, depth)
|
|
182
|
+
k = s
|
|
183
|
+
if keyed is not None and depth == 16:
|
|
184
|
+
k = [(line[n * 2] << 8) | line[n * 2 + 1] for n in range(pw * channels)]
|
|
185
|
+
base = (y0 + j * dy) * w + x0
|
|
186
|
+
for i in range(pw):
|
|
187
|
+
if ctype == 0:
|
|
188
|
+
v = s[i]
|
|
189
|
+
g = v * 255 // maxv
|
|
190
|
+
px = (g, g, g, 0 if k[i] == keyed else 255)
|
|
191
|
+
elif ctype == 2:
|
|
192
|
+
px = (s[i * 3], s[i * 3 + 1], s[i * 3 + 2], 0 if (k[i * 3], k[i * 3 + 1], k[i * 3 + 2]) == keyed else 255)
|
|
193
|
+
elif ctype == 3:
|
|
194
|
+
px = pal[s[i]] if s[i] < len(pal) else black
|
|
195
|
+
elif ctype == 4:
|
|
196
|
+
px = (s[i * 2], s[i * 2], s[i * 2], s[i * 2 + 1])
|
|
197
|
+
else:
|
|
198
|
+
px = (s[i * 4], s[i * 4 + 1], s[i * 4 + 2], s[i * 4 + 3])
|
|
199
|
+
o = (base + i * dx) * 4
|
|
200
|
+
out[o:o + 4] = bytes(px)
|
|
201
|
+
return w, h, out
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _is_bmp(d):
|
|
205
|
+
return d.startswith(b'BM')
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _bmp_channel(v, mask):
|
|
209
|
+
if not mask:
|
|
210
|
+
return 255
|
|
211
|
+
shift = (mask & -mask).bit_length() - 1
|
|
212
|
+
return ((v & mask) >> shift) * 255 // (mask >> shift)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _bmp(data):
|
|
216
|
+
off = struct.unpack_from('<I', data, 10)[0]
|
|
217
|
+
size = struct.unpack_from('<I', data, 14)[0]
|
|
218
|
+
if size == 12:
|
|
219
|
+
w, h, _, bpp = struct.unpack_from('<HHHH', data, 18)
|
|
220
|
+
comp, used, entry = 0, 0, 3
|
|
221
|
+
else:
|
|
222
|
+
w, h, _, bpp, comp, _, _, _, used, _ = struct.unpack_from('<iiHHIIiiII', data, 18)
|
|
223
|
+
entry = 4
|
|
224
|
+
topdown = h < 0
|
|
225
|
+
h = abs(h)
|
|
226
|
+
if comp not in (0, 3, 6):
|
|
227
|
+
raise ImageError('compressed BMP is not supported')
|
|
228
|
+
if bpp not in (1, 4, 8, 16, 24, 32):
|
|
229
|
+
raise ImageError('unsupported BMP bit depth')
|
|
230
|
+
out = _blank(w, h)
|
|
231
|
+
pal = []
|
|
232
|
+
if bpp <= 8:
|
|
233
|
+
start = 14 + size
|
|
234
|
+
for i in range(used or (1 << bpp)):
|
|
235
|
+
p = start + i * entry
|
|
236
|
+
if p + 3 > len(data):
|
|
237
|
+
break
|
|
238
|
+
pal.append((data[p + 2], data[p + 1], data[p], 255))
|
|
239
|
+
masks = (0, 0, 0, 0)
|
|
240
|
+
if bpp in (16, 32):
|
|
241
|
+
if comp in (3, 6) and size >= 52:
|
|
242
|
+
masks = struct.unpack_from('<III', data, 54) + (struct.unpack_from('<I', data, 66)[0] if size >= 56 else 0,)
|
|
243
|
+
elif comp in (3, 6):
|
|
244
|
+
masks = struct.unpack_from('<III', data, 14 + size) + (0,)
|
|
245
|
+
elif bpp == 16:
|
|
246
|
+
masks = (0x7C00, 0x03E0, 0x001F, 0)
|
|
247
|
+
else:
|
|
248
|
+
masks = (0xFF0000, 0xFF00, 0xFF, 0)
|
|
249
|
+
stride = ((w * bpp + 31) // 32) * 4
|
|
250
|
+
for row in range(h):
|
|
251
|
+
y = row if topdown else h - 1 - row
|
|
252
|
+
line = data[off + row * stride:off + row * stride + stride]
|
|
253
|
+
if len(line) < stride:
|
|
254
|
+
raise ImageError('truncated BMP data')
|
|
255
|
+
for x in range(w):
|
|
256
|
+
if bpp == 24:
|
|
257
|
+
px = (line[x * 3 + 2], line[x * 3 + 1], line[x * 3], 255)
|
|
258
|
+
elif bpp == 32:
|
|
259
|
+
v = line[x * 4] | line[x * 4 + 1] << 8 | line[x * 4 + 2] << 16 | line[x * 4 + 3] << 24
|
|
260
|
+
px = tuple(_bmp_channel(v, m) for m in masks)
|
|
261
|
+
elif bpp == 16:
|
|
262
|
+
v = line[x * 2] | line[x * 2 + 1] << 8
|
|
263
|
+
px = tuple(_bmp_channel(v, m) for m in masks)
|
|
264
|
+
else:
|
|
265
|
+
per = 8 // bpp
|
|
266
|
+
idx = (line[x // per] >> (8 - bpp * (x % per + 1))) & ((1 << bpp) - 1)
|
|
267
|
+
px = pal[idx] if idx < len(pal) else (0, 0, 0, 255)
|
|
268
|
+
o = (y * w + x) * 4
|
|
269
|
+
out[o:o + 4] = bytes(px)
|
|
270
|
+
return w, h, out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _is_pnm(d):
|
|
274
|
+
return len(d) > 2 and d[0:1] == b'P' and d[1:2] in b'123456' and d[2:3].isspace()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _pnm(data):
|
|
278
|
+
kind = data[1:2]
|
|
279
|
+
pos = 2
|
|
280
|
+
|
|
281
|
+
def token():
|
|
282
|
+
nonlocal pos
|
|
283
|
+
while True:
|
|
284
|
+
while pos < len(data) and data[pos:pos + 1].isspace():
|
|
285
|
+
pos += 1
|
|
286
|
+
if data[pos:pos + 1] == b'#':
|
|
287
|
+
while pos < len(data) and data[pos] not in b'\r\n':
|
|
288
|
+
pos += 1
|
|
289
|
+
else:
|
|
290
|
+
break
|
|
291
|
+
start = pos
|
|
292
|
+
while pos < len(data) and not data[pos:pos + 1].isspace():
|
|
293
|
+
pos += 1
|
|
294
|
+
return data[start:pos]
|
|
295
|
+
|
|
296
|
+
w = int(token())
|
|
297
|
+
h = int(token())
|
|
298
|
+
bitmap = kind in (b'1', b'4')
|
|
299
|
+
maxv = 1 if bitmap else int(token())
|
|
300
|
+
if maxv < 1 or maxv > 65535:
|
|
301
|
+
raise ImageError('invalid PNM maximum value')
|
|
302
|
+
channels = 3 if kind in (b'3', b'6') else 1
|
|
303
|
+
count = w * h * channels
|
|
304
|
+
out = _blank(w, h)
|
|
305
|
+
if kind == b'1':
|
|
306
|
+
vals = [0 if c == 49 else 1 for c in re.sub(rb'#[^\r\n]*', b'', data[pos:]) if c in b'01']
|
|
307
|
+
elif kind == b'4':
|
|
308
|
+
pos += 1
|
|
309
|
+
stride = (w + 7) // 8
|
|
310
|
+
vals = []
|
|
311
|
+
for y in range(h):
|
|
312
|
+
line = data[pos + y * stride:pos + (y + 1) * stride]
|
|
313
|
+
if len(line) < stride:
|
|
314
|
+
raise ImageError('truncated PNM data')
|
|
315
|
+
vals.extend(0 if (line[x // 8] >> (7 - x % 8)) & 1 else 1 for x in range(w))
|
|
316
|
+
elif kind in (b'2', b'3'):
|
|
317
|
+
vals = [int(t) for t in re.sub(rb'#[^\r\n]*', b'', data[pos:]).split()]
|
|
318
|
+
else:
|
|
319
|
+
pos += 1
|
|
320
|
+
if maxv < 256:
|
|
321
|
+
vals = data[pos:pos + count]
|
|
322
|
+
else:
|
|
323
|
+
vals = [(data[i] << 8) | data[i + 1] for i in range(pos, pos + count * 2 - 1, 2)]
|
|
324
|
+
if len(vals) < count:
|
|
325
|
+
raise ImageError('truncated PNM data')
|
|
326
|
+
scale = 1 if bitmap else maxv
|
|
327
|
+
for i in range(w * h):
|
|
328
|
+
if channels == 3:
|
|
329
|
+
px = (vals[i * 3] * 255 // scale, vals[i * 3 + 1] * 255 // scale, vals[i * 3 + 2] * 255 // scale, 255)
|
|
330
|
+
else:
|
|
331
|
+
g = vals[i] * 255 // scale
|
|
332
|
+
px = (g, g, g, 255)
|
|
333
|
+
out[i * 4:i * 4 + 4] = bytes(px)
|
|
334
|
+
return w, h, out
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _is_gif(d):
|
|
338
|
+
return d[:6] in (b'GIF87a', b'GIF89a')
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _gif_lzw(data, minbits):
|
|
342
|
+
clear = 1 << minbits
|
|
343
|
+
end = clear + 1
|
|
344
|
+
base = [bytes((i,)) for i in range(clear)] + [b'', b'']
|
|
345
|
+
table = list(base)
|
|
346
|
+
size = minbits + 1
|
|
347
|
+
prev = None
|
|
348
|
+
out = bytearray()
|
|
349
|
+
buf = 0
|
|
350
|
+
nbits = 0
|
|
351
|
+
for byte in data:
|
|
352
|
+
buf |= byte << nbits
|
|
353
|
+
nbits += 8
|
|
354
|
+
while nbits >= size:
|
|
355
|
+
code = buf & ((1 << size) - 1)
|
|
356
|
+
buf >>= size
|
|
357
|
+
nbits -= size
|
|
358
|
+
if code == clear:
|
|
359
|
+
table = list(base)
|
|
360
|
+
size = minbits + 1
|
|
361
|
+
prev = None
|
|
362
|
+
continue
|
|
363
|
+
if code == end:
|
|
364
|
+
return out
|
|
365
|
+
if prev is None:
|
|
366
|
+
entry = table[code]
|
|
367
|
+
else:
|
|
368
|
+
if code > len(table):
|
|
369
|
+
raise ImageError('corrupt GIF data')
|
|
370
|
+
entry = table[code] if code < len(table) else prev + prev[:1]
|
|
371
|
+
if len(table) < 4096:
|
|
372
|
+
table.append(prev + entry[:1])
|
|
373
|
+
if len(table) == (1 << size) and size < 12:
|
|
374
|
+
size += 1
|
|
375
|
+
out += entry
|
|
376
|
+
prev = entry
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _gif(data):
|
|
381
|
+
sw, sh, flags = struct.unpack_from('<HHB', data, 6)
|
|
382
|
+
pos = 13
|
|
383
|
+
gct = b''
|
|
384
|
+
if flags & 0x80:
|
|
385
|
+
n = 3 * (2 << (flags & 7))
|
|
386
|
+
gct = data[pos:pos + n]
|
|
387
|
+
pos += n
|
|
388
|
+
transparent = None
|
|
389
|
+
out = _blank(sw, sh)
|
|
390
|
+
while pos < len(data):
|
|
391
|
+
block = data[pos]
|
|
392
|
+
pos += 1
|
|
393
|
+
if block == 0x21:
|
|
394
|
+
label = data[pos]
|
|
395
|
+
pos += 1
|
|
396
|
+
if label == 0xF9 and data[pos] >= 4:
|
|
397
|
+
if data[pos + 1] & 1:
|
|
398
|
+
transparent = data[pos + 4]
|
|
399
|
+
while data[pos]:
|
|
400
|
+
pos += data[pos] + 1
|
|
401
|
+
pos += 1
|
|
402
|
+
elif block == 0x2C:
|
|
403
|
+
left, top, fw, fh, fflags = struct.unpack_from('<HHHHB', data, pos)
|
|
404
|
+
pos += 9
|
|
405
|
+
table = gct
|
|
406
|
+
if fflags & 0x80:
|
|
407
|
+
n = 3 * (2 << (fflags & 7))
|
|
408
|
+
table = data[pos:pos + n]
|
|
409
|
+
pos += n
|
|
410
|
+
minbits = data[pos]
|
|
411
|
+
if not 1 <= minbits <= 8:
|
|
412
|
+
raise ImageError('corrupt GIF data')
|
|
413
|
+
pos += 1
|
|
414
|
+
chunks = []
|
|
415
|
+
while data[pos]:
|
|
416
|
+
chunks.append(data[pos + 1:pos + 1 + data[pos]])
|
|
417
|
+
pos += data[pos] + 1
|
|
418
|
+
idx = _gif_lzw(b''.join(chunks), minbits)
|
|
419
|
+
if len(idx) < fw * fh:
|
|
420
|
+
idx += bytes(fw * fh - len(idx))
|
|
421
|
+
if fflags & 0x40:
|
|
422
|
+
order = [y for start, step in ((0, 8), (4, 8), (2, 4), (1, 2)) for y in range(start, fh, step)]
|
|
423
|
+
else:
|
|
424
|
+
order = range(fh)
|
|
425
|
+
for src, y in enumerate(order):
|
|
426
|
+
oy = top + y
|
|
427
|
+
if oy >= sh:
|
|
428
|
+
continue
|
|
429
|
+
for x in range(fw):
|
|
430
|
+
ox = left + x
|
|
431
|
+
if ox >= sw:
|
|
432
|
+
break
|
|
433
|
+
i = idx[src * fw + x]
|
|
434
|
+
o = (oy * sw + ox) * 4
|
|
435
|
+
if i == transparent or i * 3 + 2 >= len(table):
|
|
436
|
+
continue
|
|
437
|
+
out[o:o + 4] = bytes((table[i * 3], table[i * 3 + 1], table[i * 3 + 2], 255))
|
|
438
|
+
return sw, sh, out
|
|
439
|
+
else:
|
|
440
|
+
break
|
|
441
|
+
raise ImageError('GIF contains no image')
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
_ZIGZAG = (
|
|
445
|
+
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5,
|
|
446
|
+
12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28,
|
|
447
|
+
35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
|
|
448
|
+
58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
|
449
|
+
)
|
|
450
|
+
_IDCT = [[(1 / sqrt(2) if u == 0 else 1.0) / 2 * cos((2 * x + 1) * u * pi / 16) for u in range(8)] for x in range(8)]
|
|
451
|
+
_CLIP = bytes(400) + bytes(range(256)) + b'\xff' * 400
|
|
452
|
+
_CR_R = [int(round(1.402 * (i - 128))) for i in range(256)]
|
|
453
|
+
_CB_B = [int(round(1.772 * (i - 128))) for i in range(256)]
|
|
454
|
+
_CR_G = [-0.714136 * (i - 128) for i in range(256)]
|
|
455
|
+
_CB_G = [-0.344136 * (i - 128) for i in range(256)]
|
|
456
|
+
_JPEG_RST = re.compile(rb'\xff[\xd0-\xd7]')
|
|
457
|
+
_JPEG_END = re.compile(rb'\xff[\x01-\xcf\xd8-\xfe]')
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _is_jpeg(d):
|
|
461
|
+
return d.startswith(b'\xff\xd8')
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class _Bits:
|
|
465
|
+
__slots__ = ('d', 'p', 'buf', 'n')
|
|
466
|
+
|
|
467
|
+
def __init__(self, d):
|
|
468
|
+
self.d = d
|
|
469
|
+
self.p = 0
|
|
470
|
+
self.buf = 0
|
|
471
|
+
self.n = 0
|
|
472
|
+
|
|
473
|
+
def huff(self, lut):
|
|
474
|
+
while self.n < 16:
|
|
475
|
+
self.buf = (self.buf << 8) | (self.d[self.p] if self.p < len(self.d) else 0)
|
|
476
|
+
self.p += 1
|
|
477
|
+
self.n += 8
|
|
478
|
+
v = lut[(self.buf >> (self.n - 16)) & 0xFFFF]
|
|
479
|
+
if not v:
|
|
480
|
+
raise ImageError('corrupt JPEG data')
|
|
481
|
+
self.n -= v >> 8
|
|
482
|
+
self.buf &= (1 << self.n) - 1
|
|
483
|
+
return v & 255
|
|
484
|
+
|
|
485
|
+
def bits(self, k):
|
|
486
|
+
if not k:
|
|
487
|
+
return 0
|
|
488
|
+
while self.n < k:
|
|
489
|
+
self.buf = (self.buf << 8) | (self.d[self.p] if self.p < len(self.d) else 0)
|
|
490
|
+
self.p += 1
|
|
491
|
+
self.n += 8
|
|
492
|
+
self.n -= k
|
|
493
|
+
v = self.buf >> self.n
|
|
494
|
+
self.buf &= (1 << self.n) - 1
|
|
495
|
+
return v
|
|
496
|
+
|
|
497
|
+
def signed(self, t):
|
|
498
|
+
v = self.bits(t)
|
|
499
|
+
return v if v >= (1 << (t - 1)) else v - (1 << t) + 1
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _jpeg_lut(counts, symbols):
|
|
503
|
+
lut = [0] * 65536
|
|
504
|
+
code = 0
|
|
505
|
+
k = 0
|
|
506
|
+
for length in range(1, 17):
|
|
507
|
+
for _ in range(counts[length - 1]):
|
|
508
|
+
start = code << (16 - length)
|
|
509
|
+
span = 1 << (16 - length)
|
|
510
|
+
lut[start:start + span] = [(length << 8) | symbols[k]] * span
|
|
511
|
+
code += 1
|
|
512
|
+
k += 1
|
|
513
|
+
code <<= 1
|
|
514
|
+
return lut
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _jpeg_ac_refine(bits, lut, coef, base, ss, se, al, eobrun):
|
|
518
|
+
p1 = 1 << al
|
|
519
|
+
m1 = -1 << al
|
|
520
|
+
k = ss
|
|
521
|
+
if eobrun == 0:
|
|
522
|
+
while k <= se:
|
|
523
|
+
rs = bits.huff(lut)
|
|
524
|
+
r = rs >> 4
|
|
525
|
+
s = rs & 15
|
|
526
|
+
if s:
|
|
527
|
+
s = p1 if bits.bits(1) else m1
|
|
528
|
+
elif r != 15:
|
|
529
|
+
eobrun = 1 << r
|
|
530
|
+
if r:
|
|
531
|
+
eobrun += bits.bits(r)
|
|
532
|
+
break
|
|
533
|
+
while k <= se:
|
|
534
|
+
idx = base + _ZIGZAG[k]
|
|
535
|
+
c = coef[idx]
|
|
536
|
+
if c:
|
|
537
|
+
if bits.bits(1) and not c & p1:
|
|
538
|
+
coef[idx] = c + p1 if c >= 0 else c + m1
|
|
539
|
+
else:
|
|
540
|
+
r -= 1
|
|
541
|
+
if r < 0:
|
|
542
|
+
break
|
|
543
|
+
k += 1
|
|
544
|
+
if s and k <= 63:
|
|
545
|
+
coef[base + _ZIGZAG[k]] = s
|
|
546
|
+
k += 1
|
|
547
|
+
if eobrun > 0:
|
|
548
|
+
while k <= se:
|
|
549
|
+
idx = base + _ZIGZAG[k]
|
|
550
|
+
c = coef[idx]
|
|
551
|
+
if c and bits.bits(1) and not c & p1:
|
|
552
|
+
coef[idx] = c + p1 if c >= 0 else c + m1
|
|
553
|
+
k += 1
|
|
554
|
+
eobrun -= 1
|
|
555
|
+
return eobrun
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _jpeg_block(bits, coef, base, state, comp, dc, ac, ss, se, ah, al, progressive):
|
|
559
|
+
if not progressive:
|
|
560
|
+
t = bits.huff(dc)
|
|
561
|
+
comp['pred'] += bits.signed(t) if t else 0
|
|
562
|
+
coef[base] = comp['pred']
|
|
563
|
+
k = 1
|
|
564
|
+
while k < 64:
|
|
565
|
+
rs = bits.huff(ac)
|
|
566
|
+
r = rs >> 4
|
|
567
|
+
s = rs & 15
|
|
568
|
+
if s == 0:
|
|
569
|
+
if r != 15:
|
|
570
|
+
break
|
|
571
|
+
k += 16
|
|
572
|
+
continue
|
|
573
|
+
k += r
|
|
574
|
+
if k > 63:
|
|
575
|
+
break
|
|
576
|
+
coef[base + _ZIGZAG[k]] = bits.signed(s)
|
|
577
|
+
k += 1
|
|
578
|
+
return
|
|
579
|
+
if ss == 0:
|
|
580
|
+
if ah == 0:
|
|
581
|
+
t = bits.huff(dc)
|
|
582
|
+
comp['pred'] += bits.signed(t) if t else 0
|
|
583
|
+
coef[base] = comp['pred'] << al
|
|
584
|
+
elif bits.bits(1):
|
|
585
|
+
coef[base] |= 1 << al
|
|
586
|
+
return
|
|
587
|
+
if ah:
|
|
588
|
+
state[0] = _jpeg_ac_refine(bits, ac, coef, base, ss, se, al, state[0])
|
|
589
|
+
return
|
|
590
|
+
if state[0] > 0:
|
|
591
|
+
state[0] -= 1
|
|
592
|
+
return
|
|
593
|
+
k = ss
|
|
594
|
+
while k <= se:
|
|
595
|
+
rs = bits.huff(ac)
|
|
596
|
+
r = rs >> 4
|
|
597
|
+
s = rs & 15
|
|
598
|
+
if s == 0:
|
|
599
|
+
if r < 15:
|
|
600
|
+
state[0] = (1 << r) - 1
|
|
601
|
+
if r:
|
|
602
|
+
state[0] += bits.bits(r)
|
|
603
|
+
break
|
|
604
|
+
k += 16
|
|
605
|
+
continue
|
|
606
|
+
k += r
|
|
607
|
+
if k > 63:
|
|
608
|
+
break
|
|
609
|
+
coef[base + _ZIGZAG[k]] = bits.signed(s) * (1 << al)
|
|
610
|
+
k += 1
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _jpeg_scan(entropy, scan, comps, frame, restart, progressive):
|
|
614
|
+
ss, se, ah, al = scan['ss'], scan['se'], scan['ah'], scan['al']
|
|
615
|
+
segments = [s.replace(b'\xff\x00', b'\xff') for s in _JPEG_RST.split(entropy)]
|
|
616
|
+
sel = scan['comps']
|
|
617
|
+
if len(sel) == 1:
|
|
618
|
+
c = sel[0][0]
|
|
619
|
+
cw = -(-frame['w'] * c['h'] // frame['hmax'])
|
|
620
|
+
ch = -(-frame['h'] * c['v'] // frame['vmax'])
|
|
621
|
+
cols = -(-cw // 8)
|
|
622
|
+
total = cols * -(-ch // 8)
|
|
623
|
+
else:
|
|
624
|
+
total = frame['mcux'] * frame['mcuy']
|
|
625
|
+
state = [0]
|
|
626
|
+
seg = 0
|
|
627
|
+
bits = _Bits(segments[0])
|
|
628
|
+
for c, _, _ in sel:
|
|
629
|
+
c['pred'] = 0
|
|
630
|
+
for m in range(total):
|
|
631
|
+
if restart and m and m % restart == 0:
|
|
632
|
+
seg += 1
|
|
633
|
+
bits = _Bits(segments[seg] if seg < len(segments) else b'')
|
|
634
|
+
state[0] = 0
|
|
635
|
+
for c, _, _ in sel:
|
|
636
|
+
c['pred'] = 0
|
|
637
|
+
if len(sel) == 1:
|
|
638
|
+
c, dc, ac = sel[0]
|
|
639
|
+
by, bx = divmod(m, cols)
|
|
640
|
+
_jpeg_block(bits, c['coef'], (by * c['bw'] + bx) * 64, state, c, dc, ac, ss, se, ah, al, progressive)
|
|
641
|
+
else:
|
|
642
|
+
my, mx = divmod(m, frame['mcux'])
|
|
643
|
+
for c, dc, ac in sel:
|
|
644
|
+
for v in range(c['v']):
|
|
645
|
+
for h in range(c['h']):
|
|
646
|
+
by = my * c['v'] + v
|
|
647
|
+
bx = mx * c['h'] + h
|
|
648
|
+
_jpeg_block(bits, c['coef'], (by * c['bw'] + bx) * 64, state, c, dc, ac, ss, se, ah, al, progressive)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _jpeg_plane(c):
|
|
652
|
+
bw, bh = c['bw'], c['bh']
|
|
653
|
+
pw = bw * 8
|
|
654
|
+
plane = bytearray(pw * bh * 8)
|
|
655
|
+
q = c['q']
|
|
656
|
+
coef = c['coef']
|
|
657
|
+
zeros = [0.0] * 8
|
|
658
|
+
for by in range(bh):
|
|
659
|
+
for bx in range(bw):
|
|
660
|
+
base = (by * bw + bx) * 64
|
|
661
|
+
blk = coef[base:base + 64]
|
|
662
|
+
if not any(blk[1:]):
|
|
663
|
+
v = min(255, max(0, int(blk[0] * q[0] / 8 + 128.5)))
|
|
664
|
+
for y in range(8):
|
|
665
|
+
o = (by * 8 + y) * pw + bx * 8
|
|
666
|
+
plane[o:o + 8] = bytes((v,)) * 8
|
|
667
|
+
continue
|
|
668
|
+
blk = [a * b for a, b in zip(blk, q)]
|
|
669
|
+
tmp = []
|
|
670
|
+
for v in range(8):
|
|
671
|
+
r = blk[v * 8:v * 8 + 8]
|
|
672
|
+
tmp.append([sum(m * f for m, f in zip(row, r)) for row in _IDCT] if any(r) else zeros)
|
|
673
|
+
cols = [[sum(m * tmp[v][x] for v, m in enumerate(row)) for x in range(8)] for row in _IDCT]
|
|
674
|
+
for y in range(8):
|
|
675
|
+
o = (by * 8 + y) * pw + bx * 8
|
|
676
|
+
plane[o:o + 8] = bytes(min(255, max(0, int(s + 128.5))) for s in cols[y])
|
|
677
|
+
return plane, pw
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _jpeg(data):
|
|
681
|
+
qts = {}
|
|
682
|
+
dcs = {}
|
|
683
|
+
acs = {}
|
|
684
|
+
frame = None
|
|
685
|
+
comps = []
|
|
686
|
+
restart = 0
|
|
687
|
+
progressive = False
|
|
688
|
+
adobe = None
|
|
689
|
+
pos = 2
|
|
690
|
+
while pos + 4 <= len(data):
|
|
691
|
+
if data[pos] != 0xFF:
|
|
692
|
+
pos += 1
|
|
693
|
+
continue
|
|
694
|
+
marker = data[pos + 1]
|
|
695
|
+
if marker == 0xFF:
|
|
696
|
+
pos += 1
|
|
697
|
+
continue
|
|
698
|
+
if marker in (0xD8, 0x01) or 0xD0 <= marker <= 0xD7:
|
|
699
|
+
pos += 2
|
|
700
|
+
continue
|
|
701
|
+
if marker == 0xD9:
|
|
702
|
+
break
|
|
703
|
+
length = struct.unpack_from('>H', data, pos + 2)[0]
|
|
704
|
+
seg = data[pos + 4:pos + 2 + length]
|
|
705
|
+
pos += 2 + length
|
|
706
|
+
if marker == 0xDB:
|
|
707
|
+
i = 0
|
|
708
|
+
while i < len(seg):
|
|
709
|
+
pq, tq = seg[i] >> 4, seg[i] & 15
|
|
710
|
+
i += 1
|
|
711
|
+
if pq:
|
|
712
|
+
vals = struct.unpack_from('>64H', seg, i)
|
|
713
|
+
i += 128
|
|
714
|
+
else:
|
|
715
|
+
vals = seg[i:i + 64]
|
|
716
|
+
i += 64
|
|
717
|
+
q = [0] * 64
|
|
718
|
+
for k in range(64):
|
|
719
|
+
q[_ZIGZAG[k]] = vals[k]
|
|
720
|
+
qts[tq] = q
|
|
721
|
+
elif marker == 0xC4:
|
|
722
|
+
i = 0
|
|
723
|
+
while i < len(seg):
|
|
724
|
+
tc, th = seg[i] >> 4, seg[i] & 15
|
|
725
|
+
counts = seg[i + 1:i + 17]
|
|
726
|
+
n = sum(counts)
|
|
727
|
+
lut = _jpeg_lut(counts, seg[i + 17:i + 17 + n])
|
|
728
|
+
(acs if tc else dcs)[th] = lut
|
|
729
|
+
i += 17 + n
|
|
730
|
+
elif marker in (0xC0, 0xC1, 0xC2):
|
|
731
|
+
progressive = marker == 0xC2
|
|
732
|
+
if seg[0] != 8:
|
|
733
|
+
raise ImageError('unsupported JPEG precision')
|
|
734
|
+
h, w, n = struct.unpack_from('>HHB', seg, 1)
|
|
735
|
+
if n not in (1, 3):
|
|
736
|
+
raise ImageError('unsupported JPEG color space')
|
|
737
|
+
comps = []
|
|
738
|
+
for k in range(n):
|
|
739
|
+
cid, hv, tq = seg[6 + k * 3:9 + k * 3]
|
|
740
|
+
comps.append({'id': cid, 'h': hv >> 4, 'v': hv & 15, 'tq': tq})
|
|
741
|
+
hmax = max(c['h'] for c in comps)
|
|
742
|
+
vmax = max(c['v'] for c in comps)
|
|
743
|
+
mcux = -(-w // (8 * hmax))
|
|
744
|
+
mcuy = -(-h // (8 * vmax))
|
|
745
|
+
frame = {'w': w, 'h': h, 'hmax': hmax, 'vmax': vmax, 'mcux': mcux, 'mcuy': mcuy}
|
|
746
|
+
for c in comps:
|
|
747
|
+
c['bw'] = mcux * c['h']
|
|
748
|
+
c['bh'] = mcuy * c['v']
|
|
749
|
+
c['coef'] = array('h', bytes(2 * c['bw'] * c['bh'] * 64))
|
|
750
|
+
elif marker in (0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF):
|
|
751
|
+
raise ImageError('unsupported JPEG encoding')
|
|
752
|
+
elif marker == 0xDD:
|
|
753
|
+
restart = struct.unpack_from('>H', seg)[0]
|
|
754
|
+
elif marker == 0xEE and seg[:5] == b'Adobe':
|
|
755
|
+
adobe = seg[11]
|
|
756
|
+
elif marker == 0xDA:
|
|
757
|
+
if frame is None:
|
|
758
|
+
raise ImageError('corrupt JPEG')
|
|
759
|
+
ns = seg[0]
|
|
760
|
+
sel = []
|
|
761
|
+
for k in range(ns):
|
|
762
|
+
cid, t = seg[1 + k * 2], seg[2 + k * 2]
|
|
763
|
+
c = next(c for c in comps if c['id'] == cid)
|
|
764
|
+
sel.append((c, dcs.get(t >> 4), acs.get(t & 15)))
|
|
765
|
+
ss, se, a = seg[1 + ns * 2:4 + ns * 2]
|
|
766
|
+
scan = {'comps': sel, 'ss': ss, 'se': se, 'ah': a >> 4, 'al': a & 15}
|
|
767
|
+
m = _JPEG_END.search(data, pos)
|
|
768
|
+
end = m.start() if m else len(data)
|
|
769
|
+
_jpeg_scan(data[pos:end], scan, comps, frame, restart, progressive)
|
|
770
|
+
pos = end
|
|
771
|
+
if frame is None:
|
|
772
|
+
raise ImageError('corrupt JPEG')
|
|
773
|
+
w, h = frame['w'], frame['h']
|
|
774
|
+
planes = []
|
|
775
|
+
for c in comps:
|
|
776
|
+
c['q'] = qts[c['tq']]
|
|
777
|
+
plane, pw = _jpeg_plane(c)
|
|
778
|
+
xmap = [x * c['h'] // frame['hmax'] for x in range(w)]
|
|
779
|
+
planes.append((plane, pw, xmap, c['v']))
|
|
780
|
+
c['coef'] = None
|
|
781
|
+
out = _blank(w, h)
|
|
782
|
+
rgb = len(comps) == 3 and (adobe == 0 or [c['id'] for c in comps] == [82, 71, 66])
|
|
783
|
+
alpha = b'\xff' * w
|
|
784
|
+
for y in range(h):
|
|
785
|
+
rows = []
|
|
786
|
+
for plane, pw, xmap, v in planes:
|
|
787
|
+
o = (y * v // frame['vmax']) * pw
|
|
788
|
+
row = plane[o:o + pw]
|
|
789
|
+
rows.append([row[x] for x in xmap])
|
|
790
|
+
if len(rows) == 1:
|
|
791
|
+
r = g = b = bytes(rows[0])
|
|
792
|
+
elif rgb:
|
|
793
|
+
r, g, b = bytes(rows[0]), bytes(rows[1]), bytes(rows[2])
|
|
794
|
+
else:
|
|
795
|
+
yy, cb, cr = rows
|
|
796
|
+
r = bytes(_CLIP[400 + a + _CR_R[c]] for a, c in zip(yy, cr))
|
|
797
|
+
g = bytes(_CLIP[400 + int(a + _CB_G[b_] + _CR_G[c] + 0.5)] for a, b_, c in zip(yy, cb, cr))
|
|
798
|
+
b = bytes(_CLIP[400 + a + _CB_B[c]] for a, c in zip(yy, cb))
|
|
799
|
+
o = y * w * 4
|
|
800
|
+
out[o:o + w * 4:4] = r
|
|
801
|
+
out[o + 1:o + w * 4:4] = g
|
|
802
|
+
out[o + 2:o + w * 4:4] = b
|
|
803
|
+
out[o + 3:o + w * 4:4] = alpha
|
|
804
|
+
return w, h, out
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
_FORMATS = (
|
|
808
|
+
(_is_png, _png),
|
|
809
|
+
(_is_jpeg, _jpeg),
|
|
810
|
+
(_is_gif, _gif),
|
|
811
|
+
(_is_bmp, _bmp),
|
|
812
|
+
(_is_pnm, _pnm),
|
|
813
|
+
)
|