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