from Tkinter import * from tkMessageBox import * from tkFileDialog import * # # This is a partial program for manipulating images. It uses some features of # Python which we have not discussed, and will not. Read the lesson page for # more information, and look down for the START HERE comment. # # Background colors. bgcolor='#99FFDD' abgcolor='#BBFFEE' # Represent a pixel. Takes a #hhhhhh for the initial value. class Pixel: def __init__(self, color): self.r = int(color[1:3],16) self.g = int(color[3:5],16) self.b = int(color[5:7],16) def color(self): return '#%02x%02x%02x' % (self.r&0xff, self.g&0xff, self.b&0xff) class ImageSyncError(Exception): def __init__(self, msg): self.message = msg # Image class extending PhotoImage from Tkinter, adding convenient # methods for getting and setting individual pixels. class ImageBender(PhotoImage): # Initialize the image. This is presumably not as generaly, but I # don't know enough to fix that. Works for what I need. def __init__(self, file=''): if file == '': PhotoImage.__init__(self, width=200, height=150) self.put(('{' + (bgcolor + ' ') * 200 + '} ' )*150, (0, 0)) else: PhotoImage.__init__(self, file=file) self.cnys() # This is a Tk method omitted in Python. Perl has it! Nah nah nah nah # nah nah! I have patterned it after the write method in Tkinter.py # which sends similar arguments to Tk. It should be portable, but if # it doesn't work on Windows I'll probably want to invent some new curse # words. def data(self, background=None, format=None, from_coords=None): """Exract image data from the image in FORMAT optionally selecting the region in FROM_COORDS replacing transparent pixels with BACKGROUND. from_coords is (x.y) starting loc, or (x1,y1,x2,y2) region, excluding the limit""" args = (self.name, 'data') if background: args = args + ('-background', background) if format: args = args + ('-format', format) if from_coords: args = args + ('-from',) + tuple(from_coords) return self.tk.call(args) # Load the image data into a Python list structure. Eccch. But # going through Tk a pixel at a time is tooooo sssssllllllooooooowwwwwww # Data is loaded in (and out) a row at a time. That may turn out to have # just barely acceptable performance. def cnys(self): try: self.idat = [] for row in range(0,self.height()): # For each row, we get the row data from Tk which is a string # { #hhhhhh .... }. In the last stmt, the { } are removed, # the pixels are split, and each is converted into a an # integer. This list of integers for the row is appended to # self.idat, a list of the row lists. rowdat = self.data(background=bgcolor, from_coords=(0, row, self.width(), row + 1)) left = rowdat.find('{') right = rowdat.find('}') if left < 0 or right < 0: raise ImageSyncError("Format: Missing { or }") self.idat.append(map((lambda x: Pixel(x)), rowdat[left+1:right].split())) except Exception, e: showerror('Internal Error', 'Image sync failed: ' + str(e)) throw # Load changes made to the Python image data into the image itself. This # is a duzey, which packs the entire modified image data as a string and # sends it to self.put, which updates the image with the new data. def sync(self): self.put(' '.join(map((lambda row: '{'+ ' '.join (map((lambda x: x.color()),row)) + '}'), self.idat))) # File open. def getfile(): global pic fn = askopenfilename(filetypes=[("GIF files", '.gif')]) root.update() if fn: try: lpic = ImageBender(file=fn) except: showwarning("Open Failed", 'Unable to open ' + fn) return pic = lpic ilab.configure(image=pic) # File save def savefile(): global pic fn = asksaveasfilename(filetypes=[("GIF files", '.gif')]) root.update() if fn: try: pic.write(fn, format='GIF') except: showwarning("Save Failed", 'Unable to save to ' + fn) return # Exit def quit(): root.quit() root = Tk() root.title('Image Manipulation') bgframe = Frame(root, bg=bgcolor) bgframe.grid(row=0,column=0) # Create the standard file menu. top = Menu(bgframe, bg=bgcolor, activebackground=abgcolor) root.config(menu=top) fmenu = Menu(top, bg=bgcolor, activebackground=abgcolor) fmenu.add_command(label='Open...', command=getfile, underline=0) fmenu.add_command(label='Save...', command=savefile, underline=0) fmenu.add_command(label='Exit', command=quit, underline=0) top.add_cascade(label='File', menu=fmenu, underline=0) # Create the empty image and display it. pic = ImageBender() ilab = Label(bgframe, image=pic) ilab.grid(row=0, column=0, columnspan=3) # # *********************** START HERE ***************************** # # Pop up a not implemented warning. def sorry(): showwarning("Not Implemented", "Sorry, this function is not yet implemented") # Create and grid a button for the image modification functions. def mkbut(r, c, lab): ret = Button(bgframe, text=lab, command=sorry, bg=bgcolor, activebackground=abgcolor) ret.grid(row=r, column=c, sticky='news') return ret # Here are the six function buttons. Initially, they simply pop up the # warning that they are not implemented. lighten = mkbut(1, 0, 'Brighter') contr = mkbut(1, 1, 'Contrast') bandw = mkbut(1, 2, 'Blk & White') flip = mkbut(2, 0, 'Flip') pxled = mkbut(2, 1, 'Pixelized') red = mkbut(2, 2, 'Red') # The image is stored in a object called pic. The image is stored in an array # of pixels inside pic. Refer to any particular pixel as # pic.idat[row][column] where row and column are numbered from 0 up to, but # excluding, pic.width() and pic.height(). The pixel has parts r, g and b # which are color values from 0 to 255, which you may reference or change. # When you are done, you must call pic.sync() to make your changes effective. # Here is the implementation for the lighten function. def dolighten(): for row in range(0, pic.height()): for col in range(0, pic.width()): p = pic.idat[row][col] p.r = min(p.r + 30, 255) p.g = min(p.g + 30, 255) p.b = min(p.b + 30, 255) pic.sync() lighten.configure(command=dolighten) root.mainloop()