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

"""
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