#!/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