#! /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 PUMP = False CACHE_MAX_BYTES = 64 * 1024 * 1024 # 64MB LEFT_BORDER = 0 class Xv: def __init__(self, files_iter, fullscreen=False, clamp=False, book=False, keepscale=False, moveto=None, right2left=False, initialtab=False): self._cache = [] self.fullscreen = fullscreen self.clamp = clamp self.clampnext = initialtab self.book = book self.keepscale = keepscale if PUMP: self._files_queue = Queue.Queue(1) thread.start_new_thread(preload_files, (files_iter, self._files_queue)) else: self._files_queue = recurse(files_iter) 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.lastdisplayedimage = (None, None) self.moveto = moveto self.movedto = {} if right2left: self.right2left = -1 else: self.right2left = +1 self.right2leftorg = self.right2left self.contrast = 1.0 def getpath(self, index): if index < 0: index = 0 while index >= len(self._cache): if self._files_queue is None: next = None elif PUMP: try: next = self._files_queue.get(timeout=0.1) except Queue.Empty: self.pump_events() continue else: try: next = self._files_queue.next() except StopIteration: next = None 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, IndexError): # 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() #print list list = [(x, y) for (x, y) in list if x < 2000] #if (1024, 768) in list: # list = [(1024, 768)] #self.screen_res = list[0] # (1024, 768-16) #self.full_screen_res = list[0] #self.screen_res = (self.full_screen_res[0] - LEFT_BORDER, # self.full_screen_res[1]) 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 self.flip = False self.right2left = self.right2leftorg SCREEN_RES = self.screen_res overflow_h = SCREEN_RES[0] < float(self.image.size[0]) overflow = overflow_h or SCREEN_RES[1] < float(self.image.size[1]) if self.clampnext in [True, 'h']: if overflow: if self.clampnext == 'h': self.do_clamp_h_or_v(auto=True) else: self.do_clamp(auto=True) else: self.scale = 1.0 elif self.clamp and overflow: self.do_clamp(auto=True) if self.clampnext == 'b': self.delta_x = self.delta_y = sys.maxint else: self.delta_x = self.delta_y = -sys.maxint self.clampnext = False self.display() def do_clamp(self, func=min, auto=False): size = self.image.size if self.turn % 180 == 90: size = size[1], size[0] SCREEN_RES = self.screen_res newscale = func(SCREEN_RES[0] / float(size[0]), SCREEN_RES[1] / float(size[1])) if auto: self.scale = newscale else: self.rescale(float(newscale) / self.scale) def do_clamp_h(self, auto=False): self.do_clamp(lambda x, y: x, auto=auto) def do_clamp_h_or_v(self, auto=False): self.do_clamp(lambda x, y: min(max(x, y), 1.0), auto=auto) def do_clamp_vertically(self): SCREEN_RES = self.screen_res self.do_clamp(lambda x, y: min(1.0, max(x, y), float(y) * SCREEN_RES[0] / SCREEN_RES[1])) def imagekey(self): return (self.imagepath, self.turn % 360, self.flip, self.scale, self.contrast) def compute_current_image(self): if self.imagekey() == self.lastimage[0]: return self.lastimage[1] image = self.image if self.flip: image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT) 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 = self.convert_to_rgb(image) if self.contrast != 1.0: from PIL import ImageEnhance image = ImageEnhance.Contrast(image).enhance(self.contrast) 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 convert_to_rgb(self, image): if image.mode != "RGBA": return image.convert("RGB") w, h = image.size image2 = PIL.Image.new("RGB", (w, h), "white") BLOCKSIZE = 32 for j in range(0, h, BLOCKSIZE): for i in range(0, w, BLOCKSIZE): if ((i^j)/BLOCKSIZE) & 1: image2.paste((240,240,240), (i,j,i+BLOCKSIZE,j+BLOCKSIZE)) return PIL.Image.composite(image, image2, image) def display(self, scrolling=False): image, w, h = self.compute_current_image() self.display_image(image, w, h, scrolling=scrolling) def display_image(self, image, w, h, scrolling=False): 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) 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) def setdelta(prev, dmax): if not self.book: return dmax // 2, (False, False) if dmax >= 0 or not scrolling: d = min(prev, dmax) dmin = min(0, dmax // 2) d = max(d, dmin) return d, (d > dmin, d < dmax) else: d = min(max(prev, dmax), 0) return d, (False, False) self.delta_x, self.can_scroll_x = setdelta(self.delta_x, w-modesize[0]) self.delta_y, self.can_scroll_y = setdelta(self.delta_y, h-modesize[1]) key = (image, w, h, self.delta_x, self.delta_y) if key == self.lastdisplayedimage[0]: return if self.right2left > 0: dx = -self.delta_x else: dx = (modesize[0] - w) + self.delta_x displaypos = (dx, -self.delta_y) self.lastdisplayedimage = (key, displaypos) if self.fullscreen: self.screen.fill((0, 0, 0)) img = pygame.image.frombuffer(image.tostring(), image.size, image.mode) self.screen.blit(img, displaypos) 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.right2left self.delta_y += int(fy * h) self.display(scrolling=True) def rescale(self, f): if self.book: scrw, scrh = pygame.display.get_surface().get_size() imgw, imgh = self.lastdisplayedimage[0][1:3] if imgw > scrw: px = self.delta_x / float(imgw-scrw) self.delta_x = int(px * (imgw*f-scrw) + 0.5) if imgh > scrh: py = self.delta_y / float(imgh-scrh) self.delta_y = int(py * (imgh*f-scrh) + 0.5) self.scale *= f 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 or key == K_END or e.unicode == u'\xa7': if self.book and self.can_scroll_y[1]: self.scroll(0, +f) elif self.book and self.can_scroll_x[1]: self.scroll(f*self.right2left, -sys.maxint) else: self.curindex += 1 return if key == K_HOME: if self.book and self.can_scroll_y[0]: self.scroll(0, -f) elif self.book and self.can_scroll_x[0]: self.scroll(-f*self.right2left, sys.maxint) else: self.curindex -= 1 self.clampnext = 'b' return if key == K_BACKSPACE: self.curindex -= 1 return if key == K_RETURN: self.curindex += 1 return if key == K_TAB: if e.mod & KMOD_SHIFT: self.clampnext = 'h' else: 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.rescale(2.0) if e.unicode == u'<': self.rescale(0.5) if e.unicode == u'n': self.rescale(1.0 / self.scale) if e.unicode == u'm': self.do_clamp() if e.unicode == u'h': self.do_clamp_h_or_v() if e.unicode == u',': self.rescale(1.0905077326652577) if e.unicode == u'.': self.rescale(0.91700404320467122) if e.unicode == u';': self.rescale(1.0119850241403996) if e.unicode == u':': self.rescale(0.98815691551307305) if e.unicode == u'S': self.save_copy() if e.unicode == u'P': self.paste_screenshot_copy() if e.unicode == u'v': self.do_clamp_vertically() 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 e.unicode == u'f': self.flip = not self.flip self.turn = -self.turn self.right2left = -self.right2left self.display() if e.unicode == u'c': self.contrast *= 1.1 self.display() if e.unicode == u'C': self.contrast = 1.0 self.display() if key in (K_ESCAPE, K_q): if e.mod & KMOD_SHIFT: self._hiqualcache.clear() # useless? import gc; gc.collect() else: 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 = 'h' self.curindex += 1 def save_copy(self): try: image = self.lastimage[1][0] except TypeError: return self._save_copy(image) def _save_copy(self, image): i = 0 while True: name = 'snapshot%05d.png' % i if os.path.exists(name): i += 1 else: break image.save(name) def paste_screenshot_copy(self): try: image = self.lastimage[1][0] except TypeError: return displaypos = self.lastdisplayedimage[1] if displaypos is None: return image2 = PIL.Image.new("RGB", self.screen_res, "black") x, y = displaypos image2.paste(image, (x + LEFT_BORDER, y)) self._save_copy(image2) def string_order(s): result = [] i = 0 while i < len(s): c = s[i] if c.isdigit(): start = i i += 1 while i < len(s) and s[i] in '0123456789.': i += 1 while True: try: result.append((0, float(s[start:i]))) except ValueError: i -= 1 else: break else: if c.isalpha(): result.append((1, c.lower())) i += 1 return result 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(key=string_order) for name in names: if name.lower().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(key=string_order) 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, top=True): for path in paths: if callable(path): yield path elif os.path.isfile(path): if path.lower().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), top=False): yield t elif path.startswith('http:'): yield make_url_reader(path) elif top: yield char_device_loader(path) else: print >> sys.stderr, "%s: not a file nor a directory" % (path,) def listfulldir(dir): #print 'listdir:', dir seen = {'.svn': True} while True: try: lst = os.listdir(dir or '.') except OSError, e: print 'ignored:', e return lst.sort(key=string_order) prevlen = len(seen) for t in lst: if t not in seen: yield os.path.join(dir, t) seen[t] = True if prevlen == len(seen): break #print 'done:', dir def recurse_shuffle(paths, top=True): 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), top=False)) elif top: pending.append(iter([char_device_loader(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 sum([size for key, size in cachemru]) >= CACHE_MAX_BYTES: oldkey, _ = cachemru.pop(0) cache.pop(oldkey, None) imagekey, image, w, h = item image = image.resize((w, h), PIL.Image.ANTIALIAS) cache[imagekey] = image imagesize = 4 * w * h cachemru.append((imagekey, imagesize)) e = pygame.event.Event(REDISPLAYEVENT, imagekey=imagekey, image=image, w=w, h=h) pygame.event.post(e) def background_writer(f, g): if isinstance(f, str): f = cStringIO.StringIO(f) else: f.seek(0) while True: data = f.read(16384) if not data: break g.write(data) g.close() f.close() def convert_image_format(f): sys.stdout.write('convert...') sys.stdout.flush() child_in, child_out = os.popen2('convert - PPM:-') if isinstance(f, str): f = open(f, 'rb') 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 read_from_stdin(): data = sys.stdin.read() def stdin(): return cStringIO.StringIO(data) return [stdin] def char_device_loader(filename): def read(cache=[]): if not cache: import stat if not stat.S_ISFIFO(os.lstat(filename).st_mode): raise IOError("not a fifo") fd = os.open(filename, os.O_RDONLY | os.O_NONBLOCK) data = os.read(fd, 1) os.close(fd) if not data: raise IOError("no sender on fifo") xxx # reimplement me f = open(filename, 'rb') cache.append(f.read()) f.close() return cStringIO.StringIO(cache[0]) read.func_name = 'dev %r' % filename return read def xv(files, **kwds): x = Xv(files, **kwds) try: x.show() finally: pygame.quit() class GetOpt(object): def __init__(self, fullargs, **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(fullargs, ''.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) if not args and '-' in fullargs: args = ['-'] self.args = args def main(args, executable=''): opt = GetOpt(args, shuffle='r', fullscreen='f', clamp='m', book='b', keepscale='s', moveto='M:', order='o', right2left='2', initialtab='t') args = opt.args if not args: args = listfulldir('') if opt.shuffle: files = recurse_shuffle(args) elif opt.order: files = sort_most_recent_first(args) elif args == ['-']: files = read_from_stdin() else: files = args if os.path.basename(executable) == 'pxvm': opt.book = True opt.keepscale = True opt.right2left = True xv(files, fullscreen=opt.fullscreen or opt.book, clamp=opt.clamp, book=opt.book, keepscale=opt.keepscale, moveto=opt.moveto, right2left=opt.right2left, initialtab=opt.initialtab) if __name__ == '__main__': try: main(sys.argv[1:], sys.argv[0]) except NoFile: print >> sys.stderr, "no image." sys.exit(1)