""" 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 frame import Frame from 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 print frame.pixels 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 xrange(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