You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
7.5 KiB
343 lines
7.5 KiB
""" |
|
This module is part of the 'blup' package and defines an animation class along |
|
with the necessary functions to read animations from files. |
|
|
|
""" |
|
|
|
import re |
|
import time |
|
import xml.etree.ElementTree as ET |
|
|
|
from blup.frame import Frame |
|
from blup.frame import FrameDimension |
|
|
|
|
|
class AnimationFileError(Exception): |
|
def __init__(self, value): |
|
self.__value = value |
|
def __str__(self): |
|
return repr(self.__value) |
|
|
|
class AnimationFrame(Frame): |
|
""" This class constitutes a single frame of an animation. """ |
|
|
|
def __init__(self, dimension, delay=100): |
|
""" Initialize the frame with given dimension and delay. """ |
|
Frame.__init__(self, dimension) |
|
self.delay = int(delay) |
|
|
|
class AnimationIterator(object): |
|
""" |
|
This iterator can be used to loop over the animation's frames using |
|
python's 'for' statement. |
|
|
|
""" |
|
|
|
def __init__(self, animation): |
|
""" Initialize the iterator. """ |
|
self.__animation = animation |
|
self.__pos = 0 |
|
|
|
def __next__(self): |
|
""" Return the next frame (if available). """ |
|
if self.__pos >= len(self.__animation): |
|
raise StopIteration |
|
else: |
|
self.__pos += 1 |
|
return self.__animation[self.__pos - 1] |
|
|
|
class Animation(object): |
|
""" This class constitutes an animation. """ |
|
|
|
def __init__(self, dimension): |
|
""" Initialize the animation (without frames). """ |
|
self.__dimension = dimension |
|
self.__frames = [] |
|
self.tags = {} |
|
|
|
|
|
@property |
|
def dimension(self): |
|
return self.__dimension |
|
|
|
@property |
|
def width(self): |
|
return self.__dimension.width |
|
@property |
|
def height(self): |
|
return self.__dimension.height |
|
@property |
|
def depth(self): |
|
return self.__dimension.depth |
|
@property |
|
def channels(self): |
|
return self.__dimension.channels |
|
|
|
@property |
|
def duration(self): |
|
""" Compute the duration of the animation. """ |
|
return sum(map(lambda f: f.delay, self.__frames)) |
|
|
|
def addFrame(self, frame): |
|
""" Append a frame to the animation. """ |
|
if not isinstance(frame, Frame): |
|
raise ValueError('frame is no \'Frame\' instance') |
|
|
|
if self.dimension != frame.dimension: |
|
raise ValueError(('frame dimension %s does not match animation ' + |
|
'dimension %s' ) |
|
% (str(self.dimension), str(frame.dimension))) |
|
|
|
self.__frames.append(frame) |
|
|
|
def __iter__(self): |
|
""" Return an iterator to loop over the animation's frames. """ |
|
return AnimationIterator(self) |
|
|
|
def __len__(self): |
|
""" Return the number of frames. """ |
|
return len(self.__frames) |
|
|
|
def __getitem__(self, i): |
|
""" Retrieve a certain frame from the animation. """ |
|
if i < 0 or i >= len(self): |
|
raise IndexError('frame index out of bounds') |
|
return self.__frames[i] |
|
|
|
def __delitem__(self, i): |
|
""" Delete a certain frame from the animation. """ |
|
if i < 0 or i >= len(self): |
|
raise IndexError('frame index out of bounds') |
|
del self.__frames[i] |
|
|
|
|
|
class AnimationPlayer(object): |
|
""" |
|
This class can be used to play an Animation using an output class that |
|
implements the 'sendFrame()' function. |
|
|
|
""" |
|
|
|
def __init__(self): |
|
""" Initialize the player. """ |
|
self.__playing = False |
|
self.__elapsed = 0 |
|
|
|
@property |
|
def elapsed(self): |
|
return self.__elapsed |
|
|
|
def play(self, animation, output, count=1): |
|
""" Play the animation 'count' times on 'output'. """ |
|
self.__playing = True |
|
self.__elapsed = 0 |
|
for i in range(count): |
|
for frame in animation: |
|
output.sendFrame(frame) |
|
|
|
delay = frame.delay / 1000.0 |
|
time.sleep(delay) |
|
self.__elapsed += delay |
|
if not self.__playing: |
|
break |
|
if not self.__playing: |
|
break |
|
|
|
def stop(self): |
|
""" Stop the player, if playing. """ |
|
self.__playing = False |
|
|
|
def loadBlm(filename): |
|
""" Parse a blm file and return an Animation object. """ |
|
|
|
f = open(filename, 'r') |
|
|
|
w = h = 0 |
|
tags = {} |
|
|
|
header = True |
|
framedelay = 100 |
|
frame = [] |
|
animframes = [] |
|
for line in f: |
|
if header: |
|
m = re.match('^\s*#\s+BlinkenLights Movie (\d+)x(\d+)', line) |
|
if m: |
|
w = int(m.group(1)) |
|
h = int(m.group(2)) |
|
continue |
|
|
|
m = re.match('^\s*#\s+(.+)\s+=\s+(.+)\s+$', line) |
|
if m: |
|
key = m.group(1) |
|
val = m.group(2) |
|
tags[key] = val |
|
continue |
|
|
|
m = re.match('^@(\d+)\s+$', line) |
|
if m: |
|
if header: |
|
header = False |
|
framedelay = int(m.group(1)) |
|
if len(frame) > 0: |
|
if h == 0: |
|
h = len(frame) |
|
else: |
|
if len(frame) != h: |
|
raise AnimationFileError( |
|
'frame height does not match specified or ' + |
|
'previous height') |
|
|
|
frm = AnimationFrame(FrameDimension(w, h, 2, 1), |
|
delay=framedelay) |
|
frm.pixels = frame |
|
animframes.append(frm) |
|
frame = [] |
|
continue |
|
|
|
if not header: |
|
if line.strip() == '' or line.strip().startswith('#'): |
|
continue |
|
|
|
if re.match('^(0|1)+$', line): |
|
line = line.strip() |
|
if w == 0: |
|
w = len(line) |
|
else: |
|
if len(line) != w: |
|
raise AnimationFileError( |
|
'frame width does not match specified or ' + |
|
'previous width') |
|
row = map(lambda c: int(c), list(line)) |
|
frame.append(row) |
|
|
|
if h != 0 and len(frame) == h: |
|
frm = AnimationFrame(FrameDimension(w, h, 2, 1), |
|
delay=framedelay) |
|
frm.pixels = frame |
|
animframes.append(frm) |
|
frame = [] |
|
|
|
continue |
|
|
|
f.close() |
|
|
|
if len(frame) > 0: |
|
frm = AnimationFrame(FrameDimension(w, h, 2, 1), delay=framedelay) |
|
frm.pixels = frame |
|
animframes.append(frm) |
|
|
|
if len(animframes) == 0: |
|
raise AnimationFileError('no frames found in this file') |
|
|
|
|
|
a = Animation(FrameDimension(w, h, 2, 1)) |
|
a.tags = tags |
|
for f in animframes: |
|
a.addFrame(f) |
|
|
|
return a |
|
|
|
|
|
def loadBml(filename): |
|
""" Parse a bml file and return an Animation object. """ |
|
|
|
try: |
|
tree = ET.parse(filename) |
|
except ET.ParseError: |
|
raise AnimationFileError('invalid xml') |
|
root = tree.getroot() |
|
|
|
if root.tag != 'blm': |
|
raise AnimationFileError('the root tag needs to be a \'blm\' tag') |
|
|
|
try: |
|
width = int(root.attrib['width']) |
|
height = int(root.attrib['height']) |
|
bits = int(root.attrib['bits']) |
|
channels = int(root.attrib['channels']) |
|
|
|
dim = FrameDimension(width, height, 2**bits, channels) |
|
except KeyError as e: |
|
raise AnimationFileError('the root tag does not contain a ' + |
|
'\'%s\' attribute.' % (e[0])) |
|
|
|
if channels not in [1, 3]: |
|
raise AnimationFileError('unsupported channel count') |
|
|
|
tags = {} |
|
frames = [] |
|
|
|
for child in root: |
|
if child.tag == 'header': |
|
for headerchild in child: |
|
tags[headerchild.tag] = headerchild.text |
|
# TODO (maybe) raise an exception in case no 'title' is given |
|
|
|
elif child.tag == 'frame': |
|
delay = int(child.attrib['duration']) |
|
currentframe = AnimationFrame(dim, delay) |
|
rows = [] |
|
for row in child: |
|
charsPerPixel = channels |
|
if bits > 4: |
|
charsPerPixel *= 2 |
|
|
|
rowPixels = [] |
|
for i in range(0, width * charsPerPixel, charsPerPixel): |
|
if channels == 1: |
|
pixel = row.text[i:(i + charsPerPixel)] |
|
rowPixels.append(int(pixel, 16)) |
|
elif channels == 3: |
|
charsPerColor = charsPerPixel // 3 |
|
r = row.text[i:(i + charsPerColor)] |
|
g = row.text[(i + charsPerColor):(i + 2*charsPerColor)] |
|
b = row.text[(i + 2*charsPerColor):(i + charsPerPixel)] |
|
rowPixels.append((int(r, 16), int(g, 16), int(b, 16))) |
|
rows.append(rowPixels) |
|
currentframe.pixels = rows |
|
frames.append(currentframe) |
|
|
|
a = Animation(dim) |
|
a.tags = tags |
|
for f in frames: |
|
a.addFrame(f) |
|
|
|
return a |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load(filename): |
|
""" |
|
(Try to) parse the given file using all available parser functions and |
|
return the Animation afterwards. |
|
|
|
""" |
|
|
|
loaders = [ loadBlm, loadBml ] |
|
|
|
anim = None |
|
for loader in loaders: |
|
try: |
|
anim = loader(filename) |
|
except AnimationFileError as e: |
|
pass |
|
if anim: |
|
break |
|
|
|
if not anim: |
|
raise AnimationFileError('file could not be read') |
|
|
|
return anim |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|