text: import fractions, glob, os, pathlib, random, threading, time
  from bibliopixel.animation.matrix import Matrix
  from bibliopixel.util import log
  from bibliopixel.colors import COLORS
  from bibliopixel.colors.arithmetic import color_scale
  
  try:
      from PIL import Image, ImageSequence
  except ImportError:
      error = "Please install Python Imaging Library: pip install pillow"
      log.error(error)
  
  ROOT = pathlib.Path(__file__).parents[2]
  DEFAULT_ANIM = ROOT / 'Graphics' / 'banana.gif'
  print(DEFAULT_ANIM)
  
  class LoadNextThread(threading.Thread):
      def __init__(self, imganim):
          super().__init__()
          self.setDaemon(True)
          self._stop_event = threading.Event()
          self._wait_event = threading.Event()
          self.anim = imganim
  
      def stop(self):
          self._stop_event.set()
  
      def stopped(self):
          return self._stop_event.isSet()
  
      def loading(self):
          return self._wait_event.isSet()
  
      def loadNext(self):
          self._wait_event.set()
  
      def run(self):
          while not self.stopped():
              self._wait_event.wait()
              self.anim.loadNextGIF()
              self._wait_event.clear()
  
  
  class ImageAnim(Matrix):
      def __init__(
              self, layout, imagePath=None, offset=(0, 0), bgcolor=COLORS.Off,
              brightness=255, cycles=1, seconds=None, random=False,
              use_file_fps=True, use_gamma=True, scale_to=None, **kwds):
          """
          Animation class for displaying image animations for GIF files or a set of
          bitmaps
  
          layout:
              layout.Matrix instance
  
          imagePath:
              Path to either a single animated GIF image or folder of GIF files
  
          offset:
              X, Y coordinates of the top-left corner of the image
  
          bgcolor:
              RGB tuple color to replace any transparent pixels with.
              Avoids transparent showing as black
  
          brightness:
              Brightness value (0-255) to scale the image by.
              Otherwise uses master brightness at the time of creation
  
          use_gamma:
              If true, use the driver's gamma on the raw image data.
              TODO: why do we do this?
  
          scale_to:
              Which dimensions to scale the image to?
              None:  Don't scale
              'x':   Scale to use full width
              'y':   Scale to use full height
              'xy':  Scale both width and height
              'fit:  Use best fit from 'x' or 'y'
          """
          super().__init__(layout, **kwds)
  
          self.cycles = cycles
          self.cycle_count = 0
  
          self.seconds = seconds
          self.last_start = 0
  
          self.random = random
          self.use_file_fps = use_file_fps
  
          self._bright = brightness
          self._bgcolor = color_scale(bgcolor, self._bright)
          self._offset = offset
          self._image_buffers = [None, None]
          self._cur_img_buf = 1  # start here because loadNext swaps it
  
          self.imagePath = imagePath or str(DEFAULT_ANIM)
          self.folder_mode = os.path.isdir(self.imagePath)
          self.gif_files = []
          self.gif_indices = []
          self.folder_index = -1
          self.load_thread = None
          self.use_gamma = use_gamma
          self.scale_to = scale_to and SCALE_TO[scale_to]
  
          if self.folder_mode:
              self.gif_files = glob.glob(self.imagePath + "/*.gif")
              self.gif_indices = list(range(len(self.gif_files)))
              self.loadNextGIF()  # first load is manual
              self.swapbuf()
              self.load_thread = None
          else:
              self.loadGIFFile(self.imagePath)
              self.swapbuf()
  
      def cleanup(self, clean_layout=True):
          if self.load_thread:
              self.load_thread.stop()
          super().cleanup(clean_layout)
  
      def pre_run(self):
          if self.folder_mode:
              if not self.load_thread or not self.load_thread.is_alive():
                  self.load_thread = LoadNextThread(self)
                  self.load_thread.start()
  
              self.load_thread.loadNext()
              self.last_start = time.time()
  
          self._curImage = 0
  
      def loadGIFFile(self, gif):
          _, ext = os.path.splitext(gif)
          next_buf = self.next_img_buf()
          if not ext.lower().endswith("gif"):
              raise ValueError('Must be a GIF file!')
  
          log.debug("Loading {0} ...".format(gif))
          self._image_buffers[next_buf] = self._loadGIFSequence(gif)
  
      def _getBufferFromImage(self, img, ox, oy):
          frame = Image.new('RGBA', img.size)
          frame.paste(img)
  
          if self.scale_to:
              ix, iy = frame.size
              rx, ry = self.scale_to(fractions.Fraction(self.width, ix),
                                     fractions.Fraction(self.height, iy))
              new_size = round(rx * ix), round(ry * iy)
              resamp = Image.LANCZOS if rx * ry < 1 else Image.BICUBIC
              frame = frame.resize(new_size, resamp)
  
          duration = img.info.get('duration')
          w = min(self.layout.width - ox, frame.size[0])
          h = min(self.layout.height - oy, frame.size[1])
  
          buffer = [0] * (self.layout.numLEDs * 3)
  
          if self.use_gamma:
              gamma = self.layout.drivers[0].gamma
  
              def apply_gamma(i, color):
                  buffer[i:i + 3] = (gamma.get(c) for c in color)
          else:
              def apply_gamma(i, color):
                  buffer[i:i + 3] = color
  
          if self._bgcolor != (0, 0, 0):
              for i in range(0, 3 * self.layout.numLEDs, ):
                  apply_gamma(i, self._bgcolor)
  
          for x in range(max(ox, 0), w + ox):
              for y in range(max(oy, 0), h + oy):
                  pixel = self.layout.coord_map[y][x]
                  *color, a = frame.getpixel((x - ox, y - oy))
                  if a:
                      color = tuple((c * a) >> 8 for c in color)
                  else:
                      color = self._bgcolor
                  apply_gamma(3 * pixel, color)
  
          return (duration, buffer)
  
      def _loadGIFSequence(self, imagePath):
          img = Image.open(imagePath)
          if any(self._offset):
              ox, oy = self._offset
          elif self.scale_to:
              ox, oy = 0, 0
          else:
              ox = max(0, (self.layout.width - img.size[0]) // 2)
              oy = max(0, (self.layout.height - img.size[1]) // 2)
  
          return [self._getBufferFromImage(frame, ox, oy)
                  for frame in ImageSequence.Iterator(img)]
  
      def loadNextGIF(self):
          if self.random:
              if len(self.gif_indices) < 2:
                  self.folder_index = self.gif_indices[0]
                  self.gif_indices = list(range(len(self.gif_files)))
              else:
                  index = random.randrange(len(self.gif_indices))
                  self.folder_index = self.gif_indices.pop(index)
          else:
              self.folder_index += 1
              if self.folder_index >= len(self.gif_files):
                  self.folder_index = 0
          self.loadGIFFile(self.gif_files[self.folder_index])
  
      def next_img_buf(self):
          i = self._cur_img_buf
          i += 1
          if i > 1:
              i = 0
          return i
  
      def swapbuf(self):
          self._cur_img_buf = self.next_img_buf()
  
      def step(self, amt=1):
          self.layout.all_off()
          img = self._image_buffers[self._cur_img_buf]
  
          self.layout.setBuffer(img[self._curImage][1])
          if self.use_file_fps:
              self.internal_delay = img[self._curImage][0] / 1000.0
  
          self._curImage += 1
          if self._curImage >= len(img):
              self._curImage = 0
              if self.folder_mode:
                  loadnext = False
                  if self.seconds:
                      if ((time.time() - self.last_start) > self.seconds):
                          loadnext = True
                  else:
                      if self.cycle_count < self.cycles - 1:
                          self.cycle_count += 1
                      else:
                          loadnext = True
  
                  if loadnext and not self.load_thread.loading():
                      # wait another cycle if still loading
                      self.animComplete = True
                      self.load_thread.loadNext()
                      self.swapbuf()
                      self.cycle_count = 0
                      self.last_start = time.time()
              else:
                  self.animComplete = True
  
  
  SCALE_TO = {
      'x': lambda rx, ry: (rx, 1),
      'y': lambda rx, ry: (1, ry),
      'xy': lambda rx, ry: (rx, ry),
      'fit': lambda rx, ry: (min(rx, ry), min(rx, ry)),
  }
  