bugfixes, features, documentation, examples, new tool
[hband-tools.git] / xgui-tools / perspect
blob2ca08be66d238a57f01e440b42b6930449235efc
1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
4 import sys
5 import os
6 import gtk
7 import subprocess
8 import tempfile
9 import glib
10 import math
11 from shutil import copyfile
12 import re
13 import traceback
14 try: import better_exchook
15 except ImportError: pass
17 def add_key_binding(widget, keyname, callback):
18         accelgroup = gtk.AccelGroup()
19         key, modifier = gtk.accelerator_parse(keyname)
20         accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, callback)
21         widget.add_accel_group(accelgroup)
23 class EventImage(gtk.EventBox):
24         def __init__(self):
25                 super(self.__class__, self).__init__()
26                 self.image = gtk.Image()
27                 self.image.set_alignment(0, 0)
28                 self.add(self.image)
29         def clear(self):
30                 return self.image.clear()
31         def set_from_pixbuf(self, *args):
32                 return self.image.set_from_pixbuf(*args)
33         def set_from_file(self, *args):
34                 return self.image.set_from_file(*args)
35         def set_from_file_at_size(self, path, w, h):
36                 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(path, w, h)
37                 self.image.set_from_pixbuf(pixbuf)
38         def set_size_request(self, *args):
39                 return self.image.set_size_request(*args)
40         @property
41         def size(self):
42                 pb = self.image.get_pixbuf()
43                 return pb.get_width(), pb.get_height()
44         @property
45         def width(self):
46                 return self.size[0]
47         @property
48         def height(self):
49                 return self.size[1]
50         @property
51         def pixbuf(self):
52                 return self.image.get_pixbuf()
53         @pixbuf.setter
54         def pixbuf(self, pb):
55                 self.image.set_from_pixbuf(pb)
56         def redraw(self):
57                 self.pixbuf = self.pixbuf
59 class StockButton(gtk.Button):
60         def __init__(self, label=None, stock=None, use_underline=True, icon_size=None):
61                 if stock is not None and stock in gtk.stock_list_ids():
62                         stock_tmp = stock
63                 else:
64                         stock_tmp = gtk.STOCK_ABOUT
65                 super(self.__class__, self).__init__(stock=stock_tmp, use_underline=use_underline)
66                 if label is not None:
67                         self.set_markup(label)
68                 if stock is None:
69                         self.set_icon('')
70                 elif stock not in gtk.stock_list_ids():
71                         self.set_icon(stock)
72                 if icon_size is not None:
73                         self.set_icon(stock, icon_size)
74         def __get_children(self):
75                 align = self.get_children()[0]
76                 hbox = align.get_children()[0]
77                 return hbox.get_children()
78         def set_label(self, label):
79                 x, lbl = self.__get_children()
80                 lbl.set_label(label)
81         def set_markup(self, label):
82                 x, lbl = self.__get_children()
83                 lbl.set_markup(label)
84         def set_icon(self, icon, size=gtk.ICON_SIZE_BUTTON):
85                 img, x = self.__get_children()
86                 if type(icon) == str:
87                         if icon == '':
88                                 img.props.visible = False
89                         else:
90                                 img.set_from_icon_name(icon, size)
91                                 img.props.visible = True
92                 else:
93                         img.set_from_pixbuf(icon)
94                         img.props.visible = True
96 class Coordinate(object):
97         def __init__(self, x, y=None):
98                 if type(x) == int:
99                         assert type(y) == int
100                         self.x = x
101                         self.y = y
102                 else:
103                         assert y is None
104                         x, y = x.split(',')
105                         self.x = int(x)
106                         self.y = int(y)
107         def __str__(self):
108                 return '%d,%d' % (self.x, self.y)
109         def __repr__(self):
110                 return '<%d,%d>' % (self.x, self.y)
112 def on_clicked(widget, event):
113         x = int(min(max(0, event.x), image.width))
114         y = int(min(max(0, event.y), image.height))
115         
116         if event.button == 1:
117                 if all([c.x!=x and c.y!=y for c in coordinates]):
118                         if len(coordinates) >= 4:
119                                 remove_closest_point(x, y)
120                         coordinates.append(Coordinate(x, y))
121         elif event.button == 3:
122                 remove_closest_point(x, y)
123         refresh_display()
124         update_statusline()
126 def remove_closest_point(x, y):
127         if len(coordinates) > 0:
128                 distances = map(lambda c: {'d': (abs(c.x - x)**2 + abs(c.y - y)**2)**0.5, 'p': c}, coordinates)
129                 closest = sorted(distances, key=lambda x: x['d'])[0]['p']
130                 coordinates.remove(closest)
132 def update_statusline():
133         topleft, bottomleft, topright, bottomright = get_quadrangle_points()
134         #text = '%d:%d; %d:%d; %d:%d; %d:%d' % (topleft.x, topleft.y, topright.x, topright.y, bottomright.x, bottomright.y, bottomleft.x, bottomleft.y)
135         text = '<span color="red">⸬</span> = '
136         text += ' '.join(map(lambda c: "(%d,%d)"%(c.x, c.y), filter(lambda c: c.x >= 0, [topleft, topright, bottomright, bottomleft])))
137         if len(coordinates) > 1:
138                 leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
139                 text += '  <span color="green">▭</span> = %dx%d+%d+%d' % (rightmost-leftmost, bottommost-topmost, leftmost, topmost)
140                 
141                 vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
142                 text += '  ∡ = %0.2f° ⟂ %0.2f°' % (hangle, vangle)
143         
144         statusline.set_markup(text)
146 def refresh_display():
147         image.redraw()
148         glib.idle_add(draw_cages, priority=glib.PRIORITY_DEFAULT_IDLE)
150 def draw_cages():
151         if glob.show_quadrangle:
152                 draw_quadrangle_cage()
153         if glob.show_circumrectangle:
154                 draw_circumrectangle_cage()
156 def draw_quadrangle_cage():
157         topleft, bottomleft, topright, bottomright = get_quadrangle_points()
158         real_points = filter(lambda c: c.x >= 0, [topleft, topright, bottomright, bottomleft])
159         if len(real_points) > 0:
160                 drawable = image.window
161                 gc = gtk.gdk.GC(drawable)
162                 gc.set_rgb_fg_color(gtk.gdk.color_parse('red'))
163                 drawable.draw_lines(gc, [(c.x, c.y) for c in make_polygon(real_points)])
165 def draw_circumrectangle_cage():
166         if len(coordinates) > 1:
167                 leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
168                 drawable = image.window
169                 gc = gtk.gdk.GC(drawable)
170                 gc.set_rgb_fg_color(gtk.gdk.color_parse('green'))
171                 drawable.draw_lines(gc, [(leftmost, topmost), (rightmost, topmost), (rightmost, bottommost), (leftmost, bottommost), (leftmost, topmost)])
173 def make_polygon(points):
174         for i in range(len(points)):
175                 yield points[i-1]
176                 yield points[i]
178 def get_quadrangle_points():
179         points_left_to_right = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))
180         while len(points_left_to_right) < 4: points_left_to_right.append(Coordinate(-1, -1))
181         src_left_points  = points_left_to_right[0:2]
182         src_right_points = points_left_to_right[-2:]
183         src_topleft     = sorted(src_left_points,  cmp=lambda a, b: cmp(a.y, b.y))[0]
184         src_bottomleft  = sorted(src_left_points,  cmp=lambda a, b: cmp(a.y, b.y))[1]
185         src_topright    = sorted(src_right_points, cmp=lambda a, b: cmp(a.y, b.y))[0]
186         src_bottomright = sorted(src_right_points, cmp=lambda a, b: cmp(a.y, b.y))[1]
187         return src_topleft, src_bottomleft, src_topright, src_bottomright
189 def get_cirumrectangle():
190         leftmost   = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))[0].x
191         topmost    = sorted(coordinates, cmp=lambda a, b: cmp(a.y, b.y))[0].y
192         rightmost  = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))[-1].x
193         bottommost = sorted(coordinates, cmp=lambda a, b: cmp(a.y, b.y))[-1].y
194         return leftmost, topmost, rightmost, bottommost
196 def do_magick(distortion=True, crop=True):
197         src_topleft, src_bottomleft, src_topright, src_bottomright = get_quadrangle_points()
198         leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
199         trg_topleft     = Coordinate(leftmost, topmost)
200         trg_bottomleft  = Coordinate(leftmost, bottommost)
201         trg_topright    = Coordinate(rightmost, topmost)
202         trg_bottomright = Coordinate(rightmost, bottommost)
203         coordinatepairs = ' '.join(map(str, [
204                 src_topleft, trg_topleft, 
205                 src_topright, trg_topright,
206                 src_bottomright, trg_bottomright,
207                 src_bottomleft, trg_bottomleft,
208                 ]))
209         # "perspect-run" is there only to visualize something is running,
210         # take it out to run convert silently in the background.
211         cmd = ["perspect-run", "convert", "-verbose", glob.sourcefile, "-auto-orient"]
212         if distortion:
213                 cmd.extend(["-matte", "-virtual-pixel", "transparent", "-distort", "Perspective", coordinatepairs])
214         if crop:
215                 cmd.extend(["-crop", "%dx%d+%d+%d" % (rightmost-leftmost, bottommost-topmost, leftmost, topmost), "+repage"])
216         run_command_with_tempfile(cmd)
218 def run_command_with_tempfile(cmd):
219         _, suffix = os.path.splitext(glob.sourcefile)
220         _fd, outfile = tempfile.mkstemp(suffix=suffix)
221         cmd.extend([outfile])
222         run_command_background(cmd, callback=(cb_imagemagick, {'outfile': outfile, 'original_file': glob.sourcefile}))
224 def cb_imagemagick(err, user_params):
225         if err != 0:
226                 # don't display erro message on gtk gui because it's in a forked process.
227                 sys.stderr.write("imagemagick error: %d\n" % err)
228         else:
229                 os.environ['PERSPECT_ORIGIN_FILE'] = user_params['original_file']
230                 outfile = user_params['outfile']
231                 subprocess.Popen([sys.executable, sys.argv[0], outfile], stdout=sys.stdout, stderr=sys.stderr)
233 def run_command_background(cmd, callback=None):
234         # start convert in detached background process
235         pid1 = os.fork()
236         if pid1 == 0:
237                 os.closerange(3, 65535)
238                 pid2 = os.fork()
239                 if pid2 == 0:
240                         print cmd
241                         err = subprocess.call(cmd, stdout=sys.stdout, stderr=sys.stderr)
242                         if callback is not None:
243                                 callback[0](err, *callback[1:])
244                         os._exit(err)
245                 else:
246                         os._exit(0)
247         else:
248                 os.waitpid(pid1, 0)
250 def do_distortion_and_crop(*_):
251         do_magick(distortion=True, crop=True)
253 def do_distortion(*_):
254         do_magick(distortion=True, crop=False)
256 def do_crop(*_):
257         do_magick(distortion=False, crop=True)
259 def get_linesegment_angle(A, B):
260         tan = float(A.x - B.x) / float(A.y - B.y)
261         ang = math.degrees(math.atan(tan))
262         return ang, ang + 90 if ang <= 0 else ang - 90
264 def do_rotation_horizontal(*_):
265         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
266         cmd = get_rotation_params(hangle)
267         run_command_with_tempfile(cmd)
269 def do_rotation_vertical(*_):
270         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
271         cmd = get_rotation_params(vangle)
272         run_command_with_tempfile(cmd)
274 def do_rotation_horizontal_and_autocrop(*_):
275         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
276         cmd = get_rotation_params(hangle)
277         cmd.extend(["-fuzz", "20%", "-trim", "+repage"])
278         run_command_with_tempfile(cmd)
279         
280 def do_rotation_vertical_and_autocrop(*_):
281         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
282         cmd = get_rotation_params(vangle)
283         cmd.extend(["-fuzz", "20%", "-trim", "+repage"])
284         run_command_with_tempfile(cmd)
286 def get_rotation_params(angle):
287         cmd = ["perspect-run", "convert", glob.sourcefile, "-auto-orient", "-rotate", str(angle)]
288         return cmd
290 import imagemetadata
292 def imagerotation_cb(transformation, user_data):
293         if transformation == imagerotation.FLIP:
294                 user_data['pixbuf'] = user_data['pixbuf'].flip(horizontal=False)
295         elif transformation == imagerotation.FLOP:
296                 user_data['pixbuf'] = user_data['pixbuf'].flip(horizontal=True)
297         elif transformation == imagerotation.CLOCKWISE:
298                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.buf_ROTATE_CLOCKWISE)
299         elif transformation == imagerotation.UPSIDEDOWN:
300                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.buf_ROTATE_UPSIDEDOWN)
301         elif transformation == imagerotation.COUNTERCLOCKWISE:
302                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.buf_ROTATE_COUNTERCLOCKWISE)
303         else:
304                 #warnx("unknown image transformation code %d" % transformation)
305                 pass
307 def load_image(imageobj, sourcefile):
308         loader = gtk.gdk.PixbufLoader()
309         imagedata = open(sourcefile, 'r').read()
310         loader.write(imagedata)
311         loader.close()
312         pixbuf = loader.get_pixbuf()
313         
314         # transform the image on screen according to the file's EXIF metadata
315         rotation = {'pixbuf': pixbuf}
316         irot = imagemetadata.Image(imagedata = imagedata)
317         irot.rotate(imagerotation_cb, rotation)
318         
319         image.set_from_pixbuf(rotation['pixbuf'])
321 def do_open(*_):
322         run_command_background(["gpicview", glob.sourcefile])
324 def do_save_as(*_):
325         proposed_path = os.environ.get('PERSPECT_ORIGIN_FILE', glob.sourcefile)
326         save_path = file_choose_dialog_save(proposed_path)
327         if save_path is not None:
328                 if not os.path.exists(save_path) or question("Overwrite?\n<tt>%s</tt>"%glib.markup_escape_text(save_path), gtk.STOCK_SAVE, gtk.STOCK_CANCEL, window):
329                         try:
330                                 copyfile(glob.sourcefile, save_path)
331                         except Exception as e:
332                                 display_error(e)
335 def display_error(e):
336         text = None
337         if isinstance(e, OSError) or isinstance(e, IOError):
338                 text = '%s (#%d)' % (e.strerror, e.errno)
339                 if e.filename is not None:
340                         text += '\n%s' % (e.filename)
341         elif isinstance(e, Exception):
342                 text = e.message
343         elif type(e) == type([]):
344                 text = ''.join(e)
345         if text is None:
346                 text = str(e)
347         dlg = gtk.MessageDialog(window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
348         dlg.set_title("Error")
349         dlg.run()
350         dlg.destroy()
352 def question(msg, stock_yes=None, stock_no=None, parent=None):
353         dlg = gtk.MessageDialog(parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
354         dlg.set_markup(msg)
355         dlg.set_title("Question")
356         if stock_no is not None:
357                 dlg.get_widget_for_response(gtk.RESPONSE_NO).hide()
358                 if hasattr(stock_no, '__iter__'):
359                         btn_no = StockButton(label=stock_no[0], stock=stock_no[1])
360                 else:
361                         btn_no = StockButton(stock=stock_no)
362                 dlg.add_action_widget(btn_no, gtk.RESPONSE_NO)
363                 btn_no.show()
364         if stock_yes is not None:
365                 dlg.get_widget_for_response(gtk.RESPONSE_YES).hide()
366                 if hasattr(stock_yes, '__iter__'):
367                         btn_yes = StockButton(label=stock_yes[0], stock=stock_yes[1])
368                 else:
369                         btn_yes = StockButton(stock=stock_yes)
370                 dlg.add_action_widget(btn_yes, gtk.RESPONSE_YES)
371                 btn_yes.show()
372         resp = dlg.run()
373         dlg.destroy()
374         return (resp == gtk.RESPONSE_YES)
376 def file_choose_dialog_save(filepath):
377         global LastFolder
378         try: LastFolder
379         except NameError: LastFolder = None
380         selected = None
381         action = gtk.FILE_CHOOSER_ACTION_SAVE
382         
383         dlg = gtk.FileChooserDialog(parent=window, action=action, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT))
384         if LastFolder is not None: dlg.set_current_folder(LastFolder)
385         dlg.set_current_name(os.path.basename(filepath))
386         
387         last_resp_num = max(map(lambda a: int(getattr(gtk, a)), filter(lambda a: a.startswith('RESPONSE_'), dir(gtk))))
388         
389         resp_num_cwd = last_resp_num + 1
390         btn_cwd = StockButton(label="Working Dir", stock=gtk.STOCK_JUMP_TO)
391         dlg.add_action_widget(btn_cwd, resp_num_cwd)
392         btn_cwd.show()
393         
394         resp_num_fdir = last_resp_num + 2
395         btn_fdir = StockButton(label="Jump to File", stock=gtk.STOCK_JUMP_TO)
396         dlg.add_action_widget(btn_fdir, resp_num_fdir)
397         btn_fdir.show()
398         
399         while True:
400                 resp = dlg.run()
401                 if resp == gtk.RESPONSE_ACCEPT:
402                         selected = dlg.get_filename()
403                         break
404                 elif resp == resp_num_cwd:
405                         dlg.set_current_folder(os.getcwd())
406                 elif resp == resp_num_fdir:
407                         dlg.set_current_folder(os.path.dirname(filepath))
408                 else:
409                         break
410         LastFolder = dlg.get_current_folder()
411         dlg.destroy()
412         return selected
414 class SimpleStore(object):
415         pass
417 def toggle_quadrangle(*X):
418         glob.show_quadrangle = not glob.show_quadrangle
419         refresh_display()
421 def toggle_circumrectangle(*X):
422         glob.show_circumrectangle = not glob.show_circumrectangle
423         refresh_display()
425 def load_file(filepath):
426         load_image(image, filepath)
427         glob.sourcefile = filepath
428         window.set_title(window.get_title() + ": " + glob.sourcefile)
430 USER_LOG_FILE = '~/.perspect.coords.log'
432 def do_save_coordinates(*X):
433         curr_coords = get_quadrangle_points()
434         last_coords = read_last_saved_coordinates()
435         # append surrently pointed coordinates to the log file
436         # unless the exact same coords were saved most recently.
437         if any([cc.x != lc.x or cc.y != lc.y for cc, lc in zip(curr_coords, last_coords)]):
438                 with open(os.path.expanduser(USER_LOG_FILE), 'a') as fh:
439                         fh.write("%s %s %s %s\n" % curr_coords[0:4])
441 def read_last_saved_coordinates(*X):
442         last_coords = (Coordinate(-1, -1), Coordinate(-1, -1), Coordinate(-1, -1), Coordinate(-1, -1))
443         try:
444                 filecontext = open(os.path.expanduser(USER_LOG_FILE), 'r')
445         except (OSError, IOError) as e:
446                 traceback.print_exc()
447                 pass
448         else:
449                 with filecontext as fh:
450                         try: fh.seek(-32, 2)
451                         except IOError as e: pass
452                         while True:
453                                 pos = fh.tell()
454                                 buf = fh.read()
455                                 fh.seek(pos, 0)
456                                 if pos == 0: start_anchor_re = '^'
457                                 else: start_anchor_re = r'(?<=\n)'
458                                 matches = re.findall(start_anchor_re + r'(\S+) (\S+) (\S+) (\S+)(?=\n|$)', buf)
459                                 if matches:
460                                         print matches
461                                         last_coords = [Coordinate(x) for x in matches[-1]]
462                                         break
463                                 else:
464                                         if pos == 0: break
465                                         try: fh.seek(-32, 1)
466                                         except IOError as e: fh.seek(0, 0)
467         return last_coords
469 def do_load_coordinates(*X):
470         # replace currently pointed coordinates to the last saved ones
471         last_coords = read_last_saved_coordinates()
472         last_coords = [c for c in last_coords if not (c.x == -1 or c.y == -1)]
473         while coordinates:
474                 coordinates.pop()
475         coordinates.extend(last_coords)
476         refresh_display()
477         update_statusline()
479 def show_help(*X):
480         text = """Billentyűparancsok
482 <b>F1</b> : Perspektíva javítás
483 <b>Shift + F1</b> : Perspektíva javítás és méretre vágás
484 <b>F2</b> : Méretre vágás
485 <b>F3</b> : Forgatás úgy hogy a meghúzott vonal vízszintes legyen
486 <b>F4</b> : Forgatás úgy hogy a meghúzott vonal függőleges legyen
487 <b>Shift + F3/F4</b> : Forgatás ugyanígy és automatikus méretre vágás
489 <b>F5</b> : Kijelölt koordináták mentése <tt>"""+USER_LOG_FILE+"""</tt> fájlba
490 <b>F6</b> : Utoljára mentett koordináták visszaállítása
491 <b>Q</b> : Kijelölt négyszöget mutat/elrejt
492 <b>R</b> : Körülírt téglalapot mutat/elrejt
494 <b>Ctrl + S</b> : Aktuális fájl mentése más néven
495 <b>Ctrl + O</b> : Aktuális fájl megnyitása külső programmal
496 <b>Escape</b> : Kilépés
498 <b>Bal egérgomb</b> : Pontok megjelölése (max 4) egy szakasz vagy négyszög meghúzásához
499 <b>Jobb egérgomb</b> : A legközelebbi pont törlése
500 <b>Középső egérgomb</b> : Meghúzott szakasz vagy sokszög újrarajzoltatása
501 <b>Alt + Bal egérgomb</b> : Ablak mozgatása (ha az ablakkezelő támogatja)
503         dlg = gtk.MessageDialog(window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
504         dlg.set_markup(text)
505         dlg.set_title("Help")
506         dlg.run()
507         dlg.destroy()
511 window = gtk.Window()
512 window.set_title("Perspective correction")
513 window.connect('delete-event', lambda *x: gtk.main_quit())
514 add_key_binding(window, 'Escape', gtk.main_quit)
515 add_key_binding(window, 'question', show_help)
516 add_key_binding(window, 'r', toggle_circumrectangle)
517 add_key_binding(window, 'q', toggle_quadrangle)
518 add_key_binding(window, 'F1', do_distortion)
519 add_key_binding(window, '<Shift>F1', do_distortion_and_crop)
520 add_key_binding(window, 'F2', do_crop)
521 add_key_binding(window, 'F3', do_rotation_horizontal)
522 add_key_binding(window, '<Shift>F3', do_rotation_horizontal_and_autocrop)
523 add_key_binding(window, 'F4', do_rotation_vertical)
524 add_key_binding(window, '<Shift>F4', do_rotation_vertical_and_autocrop)
525 add_key_binding(window, 'F5', do_save_coordinates)
526 add_key_binding(window, 'F6', do_load_coordinates)
527 add_key_binding(window, '<Control>S', do_save_as)
528 add_key_binding(window, '<Control>O', do_open)
530 box1 = gtk.VBox()
531 statusline = gtk.Label()
532 statusline.set_selectable(True)
533 statusline.set_alignment(0, 0)
536 glob = SimpleStore()
537 glob.show_quadrangle = True
538 glob.show_circumrectangle = False
539 image = EventImage()
540 image.connect('button-release-event', on_clicked)
542 load_file(sys.argv[1])
546 coordinates = []
548 window.add(box1)
549 box1.pack_start(image, True, True)
550 box1.pack_start(statusline, False, True)
552 window.show_all()
553 window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
554 gtk.main()