#! /usr/bin/env python import sys, os, pygame, random, math, time import cStringIO from pygame.locals import * import PIL.Image import thread, Queue class NoFile(Exception): pass class NoMoreFile(Exception): pass REDISPLAYEVENT = USEREVENT + 1 class Xv: def __init__(self, files_iter, fullscreen=False, clamp=False, book=False, keepscale=False, moveto=None): self._cache = [] self.fullscreen = fullscreen self.clamp = clamp self.clampnext = False self.book = book self.keepscale = keepscale self._files_queue = Queue.Queue(1) thread.start_new_thread(preload_files, (files_iter, self._files_queue)) self._hiqualqueue_in = Queue.Queue() self._hiqualcache = {} args = (self._hiqualqueue_in, self._hiqualcache, []) thread.start_new_thread(high_quality_sampler, args) self.lastimage = (None, None) self.moveto = moveto self.movedto = {} def getpath(self, index): if index < 0: index = 0 while index >= len(self._cache): if self._files_queue is None: next = None else: try: next = self._files_queue.get(timeout=0.1) except Queue.Empty: self.pump_events() continue if next is not None: self._cache.append(next) else: self._files_queue = None index = len(self._cache) - 1 if index < 0: raise NoFile path = self._cache[index] return index, self.movedto.get(path, path) def getimage(self): while 1: nextindex, path = self.getpath(self.curindex) if nextindex == self.curindex-1: raise NoMoreFile self.curindex = nextindex path1 = getattr(path, '__name__', path) print path1 try: if callable(path): f = path() else: f = path image = PIL.Image.open(f) try: image.load() except IOError: # PIL's way of saying "format not supported" f = convert_image_format(f) image = PIL.Image.open(f) except (EnvironmentError, OverflowError), e: print >> sys.stderr, '%s: %s: %s' % ( path1, e.__class__.__name__, e) del self._cache[self.curindex] continue return image, path def cachecount(self): return len(self._cache) def show(self): pygame.display.init() list = pygame.display.list_modes() if (1024, 768) in list: list = [(1024, 768)] self.screen_res = list[0] self.curindex = 0 #pygame.time.set_timer(USEREVENT, 1000 * 60) while True: self.showcurrent() self.handle_events() def set_caption(self, caption): if self.fullscreen: screen = pygame.display.get_surface() if screen is None: return if caption.endswith('...'): COLOR = (0, 255, 0) else: COLOR = (255, 0, 0) RECT = (0, 0, 20, 20) screen.fill(COLOR, RECT) pygame.display.flip() screen.fill((0, 0, 0), RECT) else: pygame.display.set_caption(caption) self.curcaption = caption def showcurrent(self): self.set_caption("loading...") try: self.image, self.imagepath = self.getimage() except NoMoreFile: self.set_caption("no more image") return if self.keepscale and hasattr(self, 'scale'): pass else: self.scale = 1.0 self.turn = 0 SCREEN_RES = self.screen_res overflow = (SCREEN_RES[0] < float(self.image.size[0]) or SCREEN_RES[1] < float(self.image.size[1])) if self.clampnext: self.clampnext = False if overflow: self.do_clamp() else: self.scale = 1.0 elif self.clamp and overflow: self.do_clamp() self.delta_x = 0 self.delta_y = 0 self.display() def do_clamp(self): size = self.image.size if self.turn % 180 == 90: size = size[1], size[0] SCREEN_RES = self.screen_res self.scale = min(SCREEN_RES[0] / float(size[0]), SCREEN_RES[1] / float(size[1])) def imagekey(self): return (self.image, self.turn % 360, self.scale) def compute_current_image(self): if self.imagekey() == self.lastimage[0]: return self.lastimage[1] image = self.image self.turn %= 360 if self.turn: image = image.rotate(self.turn) w1, h1 = image.size w = max(1, int(w1 * self.scale)) h = max(1, int(h1 * self.scale)) if image.mode != "RGB": image = image.convert("RGB") key = self.imagekey() if (w, h) != (w1, h1): try: image = self._hiqualcache[key] except KeyError: item = (key, image, w, h) image = image.resize((w, h), PIL.Image.NEAREST) self._hiqualqueue_in.put(item) else: self.lastimage = (key, (image, w, h)) else: self.lastimage = (key, (image, w, h)) return image, w, h def display(self): image, w, h = self.compute_current_image() self.display_image(image, w, h) def display_image(self, image, w, h): old = pygame.display.get_surface() flags = 0 if self.fullscreen: flags |= NOFRAME modesize = self.screen_res else: modesize = w, h if old is None or (not self.fullscreen and old.get_size() != (w, h)): self.screen = pygame.display.set_mode(modesize, flags, 32) if self.fullscreen: pygame.mouse.set_visible(False) elif self.fullscreen: old.fill((0, 0, 0)) try: caption = self.imagepath.func_name except AttributeError: caption = str(self.imagepath) if self.scale != 1.0: caption += ' (%d%%)' % (int(self.scale * 100),) pygame.display.set_caption(caption) img = pygame.image.frombuffer(image.tostring(), image.size, image.mode) def setdelta(prev, dmax): self.can_scroll = False if not self.book: return dmax // 2 d = min(prev, dmax) dmin = min(0, dmax // 2) d = max(d, dmin) self.can_scroll = d < dmax return d self.delta_x = setdelta(self.delta_x, w - modesize[0]) self.delta_y = setdelta(self.delta_y, h - modesize[1]) self.screen.blit(img, (-self.delta_x, -self.delta_y)) pygame.display.flip() def scroll(self, fx, fy): if self.book: w, h = pygame.display.get_surface().get_size() self.delta_x += int(fx * w) self.delta_y += int(fy * h) self.display() def handle_events(self): self.mouselast = None self.mousedrag = None while True: e = pygame.event.wait() if e.type == MOUSEMOTION: if self.mousedrag: self.handle_mouse(e) elif e.type == KEYDOWN: key = e.key if e.mod & KMOD_CTRL: f = 0.2 elif e.mod & KMOD_SHIFT: f = 0.02 else: f = 1 if key == K_SPACE: if self.book and self.can_scroll: key = K_DOWN else: self.curindex += 1 return if key == K_BACKSPACE: self.curindex -= 1 return if key == K_TAB: self.clampnext = True self.curindex += 1 return if key == K_PAGEDOWN: self.curindex += 20 return if key == K_PAGEUP: self.curindex -= 20 return if e.unicode == u'>': self.scale *= 2.0 self.display() if e.unicode == u'<': self.scale *= 0.5 self.display() if e.unicode == u'n': self.scale = 1.0 self.display() if e.unicode == u'm': self.do_clamp() self.display() if e.unicode == u',': self.scale *= 1.1 self.display() if e.unicode == u'.': self.scale /= 1.1 self.display() if key == K_UP: self.scroll(0, -f) if key == K_DOWN: self.scroll(0, +f) if key == K_LEFT: self.scroll(-f, 0) if key == K_RIGHT: self.scroll(+f, 0) if e.unicode == u'{': self.turn += 90 self.display() if e.unicode == u'}': self.turn -= 90 self.display() if e.unicode == u'M': self.do_move() return if key in (K_ESCAPE, K_q): raise SystemExit elif e.type == MOUSEBUTTONDOWN: if e.button == 3: self.mousedrag = self.mouselast else: self.mousedrag = None self.handle_mouse(e) elif e.type == MOUSEBUTTONUP: self.mousedrag = None elif e.type == USEREVENT: self.curindex += 1 return elif e.type == REDISPLAYEVENT: if self.imagekey() == e.imagekey: self.lastimage = (e.imagekey, (e.image, e.w, e.h)) self.display_image(e.image, e.w, e.h) elif e.type == QUIT: raise SystemExit def pump_events(self): while True: e = pygame.event.poll() if e.type == NOEVENT: break elif e.type == QUIT: raise SystemExit if hasattr(self, 'curcaption'): self.set_caption(self.curcaption) def handle_mouse(self, e, RADIUS=10): return # =========== if self.mousedrag: x1, y1 = self.mousedrag x2, y2 = e.pos dist = int(math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1))) for i in range(0, dist, 3): x = x1 + (x2-x1) * i / dist y = y1 + (y2-y1) * i / dist pygame.draw.circle(self.screen, (0,0,0), (x, y), RADIUS) pygame.draw.circle(self.screen, (0,0,0), e.pos, RADIUS) pygame.display.flip() self.mousedrag = e.pos self.mouselast = e.pos def do_move(self): if not self.moveto: return path = self._cache[self.curindex] path1 = getattr(path, '__name__', path) if not os.path.exists(path1): return dir = os.path.dirname(path1) newpath = os.path.join(dir, self.moveto, os.path.basename(path1)) os.rename(path1, newpath) print 'moved to:', newpath self.movedto[path] = newpath self.clampnext = True self.curindex += 1 def make_url_reader(url): def urlreader(): g = os.popen("lynx -dump '%s'" % (url.replace("'", "'\\''"),), 'r') data = g.read() g.close() return cStringIO.StringIO(data) return urlreader def make_zip_reader(zip, name, prefix=''): def zipreader(): return cStringIO.StringIO(zip.read(name)) zipreader.__name__ = '[%s%s]' % (prefix, name) return zipreader def make_zip_readers(zip, prefix=''): names = zip.namelist() names.sort() for name in names: if name.endswith('.zip'): import zipfile nestedzipfile = cStringIO.StringIO(zip.read(name)) nestedzip = zipfile.ZipFile(nestedzipfile, 'r') for t in make_zip_readers(nestedzip, prefix+name+'/'): yield t elif not name.endswith('/'): yield make_zip_reader(zip, name, prefix) def recursezip(zippath): if os.path.getsize(zippath) == 0: return [] import zipfile try: zip = zipfile.ZipFile(zippath, 'r') except zipfile.BadZipfile: return recurse_archive(zippath, expand="unzip %s") else: return make_zip_readers(zip, zippath + '/') class UnRar(object): def __init__(self, rarpath): self.rarpath = rarpath def do(self, cmd, file=''): rarpath = self.rarpath if os.path.getsize(rarpath) == 0 and os.path.exists(rarpath+'.part'): rarpath += '.part' rarpath = "'%s'" % (rarpath.replace("'", r"'\''"),) if file: file = "'%s'" % (file.replace("'", r"'\''"),) return os.popen("unrar %s -- %s %s" % (cmd, rarpath, file), 'r') def listdir(self): f = self.do('v') for line in f: if line.startswith('--------------'): break else: raise Exception("bad format") filename = None for line in f: if line.startswith('--------------'): break if line.startswith(' '): if filename and int(line.split()[0]) > 0: yield filename filename = None continue filename = line.strip() f.close() def make_rar_reader(self, filename): def rarreader(): f = self.do('p -inul', filename) data = f.read() f.close() return cStringIO.StringIO(data) rarreader.__name__ = '[%s]' % filename return rarreader def recurse(self): seen = {} while 1: oldlen = len(seen) names = list(self.listdir()) names.sort() for filename in names: if filename not in seen: yield self.make_rar_reader(filename) seen[filename] = True if oldlen == len(seen): break def gettmp(): global gettmp import py dir = os.environ.get('PYPY_USESSION_DIR') if dir is not None: dir = py.path.local(dir) result = py.path.local.make_numbered_dir(rootdir=dir, prefix='usession-', keep=3) n = [0] def gettmp(): res = result.join(str(n[0])) n[0] += 1 res.ensure(dir=1) return res return gettmp() def recurse_archive(path, expand, tmp=None): import py path = os.path.abspath(path) tmp = tmp or gettmp() curdir = py.path.local() try: tmp.chdir() os.system(expand % (quote(path),)) finally: curdir.chdir() for t in recurse([str(tmp)]): yield t def quote(s): return "'%s'" % (s.replace("'", "'\\''"),) def recurse(paths): for path in paths: if os.path.isfile(path): if path.endswith('.zip'): for t in recursezip(path): yield t elif path.endswith('.tar.gz') or path.endswith('.tgz'): for t in recurse_archive(path, expand="tar zxfv %s"): yield t elif path.endswith('.rar'): for t in UnRar(path).recurse(): yield t else: yield path elif os.path.isdir(path): if not os.path.islink(path): for t in recurse(listfulldir(path)): yield t elif path.startswith('http:'): yield make_url_reader(path) else: print >> sys.stderr, "%s: not a file nor a directory" % (path,) def listfulldir(dir): #print 'listdir:', dir try: lst = os.listdir(dir or '.') except OSError, e: print 'ignored:', e return lst.sort() for t in lst: if t != '.svn': yield os.path.join(dir, t) def recurse_shuffle(paths): pending = [] for path in paths: if os.path.isfile(path): pending.append(iter([path])) elif os.path.isdir(path): pending.append(recurse_shuffle(listfulldir(path))) else: print >> sys.stderr, "%s: not a file nor a directory" % (path,) while pending: index = random.randrange(0, len(pending)) it = pending[index] try: v = it.next() except StopIteration: del pending[index] else: yield v def sort_most_recent_first(paths): order = [] for path in paths: try: st = os.stat(path) except OSError: print >> sys.stderr, "%s: not found" % (path,) else: order.append((st.st_mtime, path)) order.sort(reverse=True) for _, path in order: if os.path.isdir(path): for p in sort_most_recent_first(listfulldir(path)): yield p else: yield path def preload_files(files_iter, queue): for path in recurse(files_iter): if callable(path): queue.put(path) else: try: f = open(path, 'rb') alldata = f.read() f.close() except (OSError, IOError), e: print 'ignored:', e continue def open_me(path=path, preloaded=[alldata]): if preloaded: return cStringIO.StringIO(preloaded.pop()) else: return open(path, 'rb') open_me.func_name = path queue.put(open_me) del alldata queue.put(None) def high_quality_sampler(queue, cache, cachemru): while True: hq_resample_once(queue, cache, cachemru) def hq_resample_once(queue, cache, cachemru): item = None try: while True: item = queue.get(block=(item is None)) except Queue.Empty: pass while len(cachemru) >= 5: oldkey = cachemru.pop(0) cache.pop(oldkey, None) imagekey, image, w, h = item image = image.resize((w, h), PIL.Image.ANTIALIAS) cache[imagekey] = image cachemru.append(imagekey) e = pygame.event.Event(REDISPLAYEVENT, imagekey=imagekey, image=image, w=w, h=h) pygame.event.post(e) def background_writer(f, g): while True: data = f.read(16384) if not data: break g.write(data) g.close() f.close() def convert_image_format(f): f.seek(0) sys.stdout.write('convert...') sys.stdout.flush() child_in, child_out = os.popen2('convert - PPM:-') thread.start_new_thread(background_writer, (f, child_in)) data = child_out.read() child_out.close() sys.stdout.write(' done\n') return cStringIO.StringIO(data) def xv(files, **kwds): x = Xv(files, **kwds) x.show() class GetOpt(object): def __init__(self, args, **kwds): from getopt import gnu_getopt as getopt keys = [] for long, short in kwds.items(): if short.endswith(':'): long += '=' keys.append(long) options, args = getopt(args, ''.join(kwds.values()), keys) mapping = {} for long, short in kwds.items(): witharg = short.endswith(':') if witharg: short = short[:-1] mapping['-'+short] = mapping['--'+long] = (long, witharg) setattr(self, long, None) for option, value in options: name, witharg = mapping[option] if not witharg: value = True setattr(self, name, value) self.args = args def main(args): opt = GetOpt(args, shuffle='r', fullscreen='f', clamp='m', book='b', keepscale='s', moveto='M:', order='o') args = opt.args if not args: args = listfulldir('') if opt.shuffle: files = recurse_shuffle(args) elif opt.order: files = sort_most_recent_first(args) else: files = args xv(files, fullscreen=opt.fullscreen or opt.book, clamp=opt.clamp, book=opt.book, keepscale=opt.keepscale, moveto=opt.moveto) if __name__ == '__main__': try: main(sys.argv[1:]) except NoFile: print >> sys.stderr, "no image." sys.exit(1)