#!/usr/bin/env pythonw import sys import os import pygame import pygame.constants import copy # can't make this too low or it doesn't get done right... at 5, it seems to trigger too soon display_paint_latency = 20 # ms # old helper dict for key bindings # PK = dict([(k.replace("K_",""), getattr(pygame.constants,k)) for k in dir(pygame.constants) if k.startswith("K_")]) class pygame_key_helper: def __init__(self, lead): for k in dir(pygame.constants): if k.startswith(lead): self.__dict__[k.replace(lead,"")] = getattr(pygame.constants,k) PK = pygame_key_helper("K_") PKMOD = pygame_key_helper("KMOD_") import pygame.constants as PC def NS_replace_icon(): import AppKit # setting the app name/title as well would be nice, but doesn't seem possible from here ipath = __file__.replace(".py","") + ".icns" img = AppKit.NSImage.alloc().initWithContentsOfFile_(ipath) if img: pygame.macosx.app.setApplicationIconImage_(img) import captfile # viewimage.changepath def changepath(imagepath, offset, names=[]): # ~/PIX/SL300RT/downcase/100cxbox/kicx0257.jpg.toenail d = os.path.dirname(imagepath) # use epath, later if not d: d = "." b = os.path.basename(imagepath) # even with osx' caching of directories, it is much faster to cache this too: if not names: names[:] = [f for f in os.listdir(d) if f.lower().endswith(".jpg")] names.sort() # is this lot below avoidable if we simply do names.append(names[0]) after sort? try: return os.path.join(d,names[names.index(b)+offset]) except IndexError: if offset > 0: return os.path.join(d,names[0]) else: return os.path.join(d,names[-1]) def next_uncaptioned_path(imagepath, names=[]): # should steal cache from changepath! d = os.path.dirname(imagepath) # use epath, later if not d: d = "." b = os.path.basename(imagepath) if not names: names[:] = [f for f in os.listdir(d) if f.lower().endswith(".jpg")] names.sort() fwnames = names[names.index(b):] for n in fwnames: nn = os.path.join(d, n) if not os.path.exists(captfile.capt_path_of_image(nn)): return nn # signal somehow return imagepath def old_make_pygame_image(ipath): image = pygame.image.load(ipath) return image import PIL import PIL.Image def make_pygame_image(ipath): if os.path.exists(ipath + ".toenail"): return old_make_pygame_image(ipath + ".toenail") srcim = PIL.Image.open(ipath) srcx, srcy = srcim.size # parameterize on window size rat = srcx/579.0 if srcy/rat > 439: rat = srcy/439.0 srcim.thumbnail((int(srcx/rat), int(srcy/rat))) image = pygame.image.fromstring(srcim.tostring("raw"), srcim.size, "RGB") # and stash a toenail here... srcim.save(ipath + ".toenail", "JPEG") return image def flip_file(ipath, surf, direction=0): # if the label hasn't triggered yet, defer it pygame.time.set_timer(PC.USEREVENT, 0) if direction: ipath = changepath(ipath, direction) image = make_pygame_image(ipath) iconv = image.convert() surf.blit(iconv, (0,0)) pygame.display.flip() # trigger for display paint pygame.time.set_timer(PC.USEREVENT, display_paint_latency) return ipath class display_surface: def __init__(self, surf): self.display_surf = surf self.overlay_surf = None self.base_surf = None # operations that used to be direct get copied... def get_height(self): return self.display_surf.get_height() def get_width(self): return self.display_surf.get_width() # but blit is special: def cls(self): self.overlay_surf = self.display_surf.convert_alpha() self.overlay_surf.fill((0,0,0,0)) # self.overlay_surf.set_alpha(128) def blit(self, rect, coord): self.overlay_surf.blit(rect, coord) def flip(self): self.display_surf.blit(self.base_surf, (0,0)) self.display_surf.blit(self.overlay_surf, (0,0)) pygame.display.flip() def base_picture(self, ipath): image = make_pygame_image(ipath) self.base_surf = image.convert() # alpha?? self.display_surf.blit(self.base_surf, (0,0)) pygame.display.flip() def flip_file(self, ipath, direction=0): # if the label hasn't triggered yet, defer it pygame.time.set_timer(PC.USEREVENT, 0) if direction: ipath = changepath(ipath, direction) self.base_picture(ipath) # trigger for display paint pygame.time.set_timer(PC.USEREVENT, display_paint_latency) return ipath class caption_painter: def __init__(self, surf): pygame.font.init() self.surf = surf # this is the *display* surface self.setfont() self.setcolors() self.setcorner("ul") def setfont(self, name="Geneva", size=20): # print "setfont:", name, size self.fontname = name self.fontsize = size self.font = pygame.font.SysFont(self.fontname, self.fontsize) def font_bigger(self): self.setfont(size = int(self.fontsize + 2)) def font_smaller(self): # > 1 should be enough, but pygame faults rendering, see footnote if self.fontsize > 6: self.setfont(size = int(self.fontsize - 1)) def setcolors(self, fore="black", back="grey", highback="orange"): self.forename = fore self.backname = back self.highname = highback self.textcolor = pygame.color.Color(self.forename) self.backcolor = pygame.color.Color(self.backname) self.highcolor = pygame.color.Color(self.highname) def setcorner(self, name=None): if name: self.corner = name else: corners = ["ul", "ll", "lr", "ur"] corners.append(corners[0]) # make it a ring self.corner = corners[corners.index(self.corner)+1] def paint(self, ipath, savepath=[]): if not savepath or savepath[0] != ipath: self.title(ipath) self.load_caption(ipath) savepath[:] = [ipath] self.draw_caption(ipath) def title(self, ipath): # pygame.display.set_caption(os.path.basename(ipath).replace(".toenail", " (medium)")) pygame.display.set_caption(os.path.basename(ipath)) def load_caption(self, ipath): self.saved_capts = "" if hasattr(self, "capts"): self.saved_capts = copy.copy(self.capts) # or just save the real fields? fields, vals = captfile.capt2dict(ipath) self.capts = ["%s: %s" % (f, vals[f]) for f in fields] self.origcapts = copy.copy(self.capts) self.ipath = ipath self.highidx = -1 self.caret = 0 def autofill(self): fields, vals = captfile.captcapts(self.capts) saved_fields, saved_vals = captfile.captcapts(self.saved_capts) for f in saved_fields: if (f not in fields) or not vals[f].strip(): vals[f] = saved_vals[f] if f not in fields: fields.append(f) self.capts = ["%s: %s" % (f, vals[f]) for f in fields] def revert_capts(self): self.capts = copy.copy(self.origcapts) self.highidx = -1 self.caret = 0 def modified_capts(self): if self.capts != self.origcapts: sys.stderr.write('\7') # need a pygame beep too? return 1 return None def save_caption(self, ipath): # capts is always in "display", but captfields takes file lines... # fix this interface on the captfile side? fields, vals = captfile.captcapts(self.capts) # maybe we should be passing the string, but... captfile.dict2capt(ipath, fields, vals) self.origcapts = copy.copy(self.capts) self.highidx = -1 self.caret = 0 def draw_caption(self, ipath): wmax = 0 htot = 0 for capt in self.capts: try: w,h = self.font.size(capt) except: print "Exception on", repr(capt) raise wmax = max(wmax, w + 1) # leave room for cursor htot += h if self.corner == "ul": x,y = (0,0) elif self.corner == "ll": x,y = (0, self.surf.get_height() - htot) elif self.corner == "lr": x,y = (self.surf.get_width()-wmax, self.surf.get_height() - htot) elif self.corner == "ur": x,y = (self.surf.get_width()-wmax, 0) else: print >> sys.stderr, "Bad Corner!", self.corner return self.surf.cls() for c in range(len(self.capts)): # 1 for "antialiasing on" capt = self.capts[c] if c == self.highidx: cleft, cright = capt[:self.caret], capt[self.caret:] captionlineleft = self.font.render(cleft, 1, self.textcolor, self.highcolor) captionlinecaret = self.font.render("", 1, self.highcolor, self.textcolor) captionlineright = self.font.render(cright, 1, self.textcolor, self.highcolor) #captionlineleft.set_alpha(192) #captionlinecaret.set_alpha(192) #captionlineright.set_alpha(192) self.surf.blit(captionlineleft, (x,y)) self.surf.blit(captionlinecaret, (x + captionlineleft.get_width(),y)) self.surf.blit(captionlineright, (x + captionlineleft.get_width() + captionlinecaret.get_width(),y)) y += captionlineleft.get_height() else: captionline = self.font.render(capt, 1, self.textcolor, self.backcolor) #captionline.set_alpha(192) self.surf.blit(captionline, (x,y)) y += captionline.get_height() self.surf.flip() # pygame.display.flip() def changecapt(self, offset): if len(self.capts) == 0: return self.highidx += offset if self.highidx < 0: self.highidx = len(self.capts) - 1 if self.highidx >= len(self.capts): self.highidx = 0 if len(self.capts): try: self.caret = self.capts[self.highidx].index(": ") + 2 except ValueError: self.caret = len(self.capts[self.highidx]) def nextcapt(self): self.changecapt(+1) def prevcapt(self): self.changecapt(-1) def next_or_new_capt(self): if self.highidx == len(self.capts) - 1: self.capts.append("") self.changecapt(+1) def safe_set_caret(self, newcaret): if newcaret < 0: return if self.highidx < 0: return if self.highidx > len(self.capts) - 1: return if newcaret > len(self.capts[self.highidx]): return self.caret = newcaret def movecaret(self, offset): self.safe_set_caret(self.caret + offset) def rightcaret(self): self.movecaret(+1) def leftcaret(self): self.movecaret(-1) def end_of_line_caret(self): if self.highidx < 0: return if self.highidx > len(self.capts) - 1: return self.safe_set_caret(len(self.capts[self.highidx])) def start_of_line_caret(self): self.caret = 0 def kill_to_end_of_line(self): if self.highidx < 0: return if self.highidx > len(self.capts) - 1: return if self.caret < 0: return if self.caret > len(self.capts[self.highidx]): return self.capts[self.highidx] = self.capts[self.highidx][:self.caret] def insertchar(self, uchar): if self.highidx < 0: self.next_or_new_capt() capt = self.capts[self.highidx] cleft, cright = capt[:self.caret], capt[self.caret:] self.capts[self.highidx] = cleft + uchar + cright self.caret += 1 def delchar(self): if self.highidx < 0: self.nextcapt() if self.caret == 0: return capt = self.capts[self.highidx] self.capts[self.highidx] = capt[:self.caret-1] + capt[self.caret:] self.caret -= 1 def fwdelchar(self): if self.highidx < 0: self.nextcapt() if self.caret == 0: return capt = self.capts[self.highidx] if self.caret == len(capt): return self.capts[self.highidx] = capt[:self.caret] + capt[self.caret+1:] # -------------------------------------------------------------------------------- def old_paint(self, ipath): # pygame.display.set_caption(os.path.basename(ipath).replace(".toenail", " (medium)")) pygame.display.set_caption(os.path.basename(ipath)) fields, vals = captfile.capt2dict(ipath) capts = ["%s: %s" % (f, vals[f]) for f in fields] wmax = 0 htot = 0 for capt in capts: w,h = self.font.size(capt) wmax = max(wmax, w) htot += h if self.corner == "ul": x,y = (0,0) elif self.corner == "ll": x,y = (0, self.surf.get_height() - htot) elif self.corner == "lr": x,y = (self.surf.get_width()-wmax, self.surf.get_height() - htot) elif self.corner == "ur": x,y = (self.surf.get_width()-wmax, 0) else: print >> sys.stderr, "Bad Corner!", self.corner return for capt in capts: # 1 for "antialiasing on" captionline = self.font.render(capt, 1, self.textcolor, self.backcolor) captionline.set_alpha(192) self.surf.blit(captionline, (x,y)) y += captionline.get_height() pygame.display.flip() # obsoleted by the above, but saving it until I get it checked in def paint_file_capt(ipath, surf, fontcache = []): # set up the constants textcolor = pygame.color.Color("white") backcolor = pygame.color.Color("grey") if not fontcache: pygame.font.init() fontcache.append(pygame.font.SysFont("Geneva", 18)) font = fontcache[0] # draw the lines # capts = ["Filename: %s" % ipath, "Size: %s" % os.path.getsize(ipath)] fields, vals = captfile.capt2dict(ipath) capts = ["%s: %s" % (f, vals[f]) for f in fields] # print ipath, capts y = 0 for capt in capts: # 1 for "antialiasing on" captionline = font.render(capt, 1, textcolor, backcolor) captionline.set_alpha(192) surf.blit(captionline, (0,y)) y += captionline.get_height() pygame.display.flip() def is_text_key(ev): # probably more advanced, but start here... return len(ev.unicode) > 0 # # remaining features: # [done]m-a "fill" # more editing strokes as I need them # completion on field names # completion on people names # completion/history on location names? or just smart-edit? # ability to insert chars with modifiers, like \"o # [done]cheat: special case them one-by-one # # [done]do overlay text # [done]TAB to start, and cycle rows (RET too?) # text edit in the value only - [done]c-a, [done]c-e, m-b, m-f, c-b, c-f, [done]del, m-del # [done] did c-k, just to clean up option-char hacking # [done]c-s/m-s to save # [done]block next/prev if changes outstanding? or just make it modeful? # # # # do "find exif thumbnail", and display that? [they're only 160x120, too small] # [cheated, used open]do "switch to fullscreen and load the real image" - [done]need pil, maybe? # # also someday: pre-fetch the directory listing? perhaps in a subthread or something? # # last-changed pointer - or "next-uncaptioned"? # c-d # maybe tab should be fields again, not move-view # line wrap! def main(ipath): pygame.display.set_caption("viewImage") pygame.display.set_mode((579,434)) # from toenail size # or use pygame.image.fromstring on something from PIL dsurf = display_surface(pygame.display.get_surface()) dsurf.base_picture(ipath) # really aggressive repeat for fast browsing pygame.key.set_repeat(500,1) # trigger for display paint pygame.time.set_timer(PC.USEREVENT, display_paint_latency) captions = caption_painter(dsurf) edit_commands = [ [PKMOD.CTRL, PK.TAB, captions.setcorner], [PKMOD.META, PK.MINUS, captions.font_smaller], [PKMOD.META, PK.EQUALS, captions.font_bigger], [0, PK.UP, captions.prevcapt], [0, PK.DOWN, captions.nextcapt], [0, PK.TAB, captions.nextcapt], # duplicate, but I keep hitting it [0, PK.RETURN, captions.next_or_new_capt], [0, PK.LEFT, captions.leftcaret], [PKMOD.CTRL, PK.b, captions.leftcaret], [0, PK.RIGHT, captions.rightcaret], [PKMOD.CTRL, PK.f, captions.rightcaret], [0, PK.BACKSPACE, captions.delchar], [PKMOD.CTRL, PK.d, captions.fwdelchar], [PKMOD.CTRL, PK.a, captions.start_of_line_caret], [PKMOD.CTRL, PK.e, captions.end_of_line_caret], [PKMOD.CTRL, PK.k, captions.kill_to_end_of_line], [PKMOD.META, PK.r, captions.revert_capts], [PKMOD.META, PK.a, captions.autofill], # [PKMOD.META, PK.i, captions.fill_default_titles], ] def edit_command(ev): for mod, key, verb in edit_commands: if ((not mod) or (mod and (mod & ev.mod))) and (key == ev.key): # dsurf.flip_file(ipath) verb() captions.paint(ipath) return 1 return None while 1: for event in pygame.event.get(): # sys.stderr.write(".") if event.type == PC.QUIT: print "Done." print >> open(os.path.expanduser("~/.pygimage.last"),"w"), ipath return elif event.type == PC.USEREVENT: # this event is a one-shot, so stop the re-trigger immediately pygame.time.set_timer(PC.USEREVENT, 0) captions.paint(ipath) elif event.type == PC.KEYDOWN: # print event, len(event.unicode) # print pygame.key.name(event.key) # <Event(2-KeyDown {'key': 304, 'unicode': u'', 'mod': 0})> <type 'Event'> # <Event(2-KeyDown {'key': 113, 'unicode': u'Q', 'mod': 1})> <type 'Event'> if event.key == PK.ESCAPE: # should be for modes, later return meta = (event.mod & PKMOD.META) and not event.mod & ~PKMOD.META ctrl = (event.mod & PKMOD.CTRL) and not event.mod & ~PKMOD.CTRL ctrlmeta = ((event.mod & PKMOD.CTRL) and (event.mod & PKMOD.META) and not (event.mod & ~(PKMOD.CTRL|PKMOD.META))) # if not meta and not ctrl and event.mod: # print "extra ctl:", event.mod, PKMOD.__dict__.items() if meta and event.key == PK.q: print "Done." print >> open(os.path.expanduser("~/.pygimage.last"),"w"), ipath return elif meta and event.key == PK.n: if not captions.modified_capts(): ipath = dsurf.flip_file(ipath, +1) elif meta and event.key == PK.p: if not captions.modified_capts(): ipath = dsurf.flip_file(ipath, -1) elif meta and event.key == PK.s: dsurf.flip_file(ipath) captions.save_caption(ipath) elif meta and event.key == PK.f: # implement Full Screen view some day # maybe just spawnv open? os.spawnlp(os.P_NOWAIT, "open", "open", ipath) elif meta and event.key == PK.RIGHT: ipath = next_uncaptioned_path(ipath) dsurf.flip_file(ipath) # elif event.key == PK.TAB: # captions.setcorner() # flip_file(ipath, dsurf) elif ctrlmeta and event.key == PK.a: dsurf.flip_file(ipath) captions.autofill() captions.save_caption(ipath) ipath = dsurf.flip_file(ipath, +1) elif edit_command(event): pass # elif event.key == PK.UP: # flip_file(ipath, dsurf) # captions.changecapt(-1) # elif event.key == PK.DOWN or event.key == PK.RETURN: # flip_file(ipath, dsurf) # captions.changecapt(+1) # elif event.key == PK.LEFT: # flip_file(ipath, dsurf) # captions.movecaret(-1) # elif event.key == PK.RIGHT: # flip_file(ipath, dsurf) # captions.movecaret(+1) # elif event.key == PK.BACKSPACE: # flip_file(ipath, dsurf) # captions.delchar() elif is_text_key(event): # print "text:", event, pygame.key.name(event.key), len(event.unicode) # dsurf.flip_file(ipath) uchar = event.unicode try: uchar.encode("iso-8859-1") if uchar == u"ø": uchar = u"ö" # I need this more... captions.insertchar(uchar) captions.paint(ipath) except UnicodeEncodeError: print >> sys.stderr, "Can't represent!", repr(event.unicode) else: # print event, pygame.key.name(event.key), len(event.unicode) pass # the SL300RT includes 160x120 JPEG snapshots, but that's too small to be useful # maybe as a load-and-expand, then higher-res load of toenail later? if __name__ == "__main__": pygame.init() try: NS_replace_icon() except: pass try: path = sys.argv[1] except IndexError: path = open(os.path.expanduser("~/.pygimage.last"),"r").readline().strip() main(path) pygame.quit() # fault from rendering tiny fonts, then growing them larger: ## *** malloc[9653]: Deallocation of a pointer not malloced: 0x1903a02; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug ## *** malloc[9653]: Deallocation of a pointer not malloced: 0x4fb7600; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug ## *** malloc[9653]: Deallocation of a pointer not malloced: 0xf7ffe330; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug ## *** malloc[9653]: Deallocation of a pointer not malloced: 0x190da36; This could be a double free(), or free() called with the middle of an allocated block; Try setting environment variable MallocHelp to see tools to help debug ## Pygame Parachute Traceback: ## File "./pygimage.py", line 183, in main ## Fatal Python error: (pygame parachute) Segmentation Fault ## Abort trap # going up from 4, which didn't render: ## setfont: Geneva 4 ## Pygame Parachute Traceback: ## File "./pygimage.py", line 118, in paint ## Fatal Python error: (pygame parachute) Bus Error ## Abort trap