[Pycodeocr-main] SF.net SVN: pycodeocr:[27]
Status: Beta
Brought to you by:
drtrigon
From: <la...@us...> - 2010-10-25 18:39:38
|
Revision: 27 http://pycodeocr.svn.sourceforge.net/pycodeocr/?rev=27&view=rev Author: laserb Date: 2010-10-25 18:39:31 +0000 (Mon, 25 Oct 2010) Log Message: ----------- Initial support for Doxygen / added blacklist for devices / added checksum test for 42 digit OCR codes , calculates the last digit before the ">" which is a checksum for the amount before, returns an error if this failes / some minor fixes / !! AND support for bigger image view !! Modified Paths: -------------- PyCodeOCR.glade PyCodeOCR.py Added Paths: ----------- __init__.py blacklist checksum.py Modified: PyCodeOCR.glade =================================================================== --- PyCodeOCR.glade 2010-09-12 19:05:23 UTC (rev 26) +++ PyCodeOCR.glade 2010-10-25 18:39:31 UTC (rev 27) @@ -1,4 +1,4 @@ -<?xml version="1.0"?> +<?xml version="1.0" encoding="UTF-8"?> <glade-interface> <!-- interface-requires gtk+ 2.16 --> <!-- interface-naming-policy toplevel-contextual --> @@ -31,7 +31,7 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="editable">False</property> - <property name="invisible_char">●</property> + <property name="invisible_char">●</property> </widget> <packing> <property name="x">16</property> @@ -39,17 +39,6 @@ </packing> </child> <child> - <widget class="GtkImage" id="image1"> - <property name="width_request">504</property> - <property name="height_request">84</property> - <property name="visible">True</property> - </widget> - <packing> - <property name="x">16</property> - <property name="y">75</property> - </packing> - </child> - <child> <widget class="GtkLabel" id="label5"> <property name="width_request">90</property> <property name="height_request">21</property> @@ -266,7 +255,7 @@ <property name="use_underline">True</property> <property name="active">True</property> <property name="draw_indicator">True</property> - <signal name="toggled" handler="on_radiobutton2_toggled"/> + <signal name="toggled" handler="on_combobox2_changed"/> </widget> <packing> <property name="x">534</property> @@ -283,7 +272,7 @@ <property name="use_underline">True</property> <property name="draw_indicator">True</property> <property name="group">radiobutton2</property> - <signal name="toggled" handler="on_radiobutton1_toggled"/> + <signal name="toggled" handler="on_combobox2_changed"/> </widget> <packing> <property name="x">318</property> @@ -317,6 +306,25 @@ <property name="y">20</property> </packing> </child> + <child> + <widget class="GtkEventBox" id="eventbox2"> + <property name="width_request">505</property> + <property name="height_request">84</property> + <property name="visible">True</property> + <signal name="button_press_event" handler="on_eventbox2_button_press_event"/> + <child> + <widget class="GtkImage" id="image1"> + <property name="width_request">505</property> + <property name="height_request">84</property> + <property name="visible">True</property> + </widget> + </child> + </widget> + <packing> + <property name="x">16</property> + <property name="y">75</property> + </packing> + </child> </widget> </child> </widget> @@ -371,4 +379,30 @@ </widget> </child> </widget> + <widget class="GtkWindow" id="window3"> + <property name="type">popup</property> + <property name="window_position">mouse</property> + <property name="destroy_with_parent">True</property> + <child> + <widget class="GtkFixed" id="fixed1"> + <property name="visible">True</property> + <child> + <widget class="GtkEventBox" id="eventbox1"> + <property name="width_request">600</property> + <property name="height_request">600</property> + <property name="visible">True</property> + <property name="events">GDK_BUTTON_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK</property> + <child> + <widget class="GtkImage" id="image2"> + <property name="width_request">600</property> + <property name="height_request">600</property> + <property name="visible">True</property> + <signal name="button_press_event" handler="on_eventbox2_button_press_event"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> </glade-interface> Modified: PyCodeOCR.py =================================================================== --- PyCodeOCR.py 2010-09-12 19:05:23 UTC (rev 26) +++ PyCodeOCR.py 2010-10-25 18:39:31 UTC (rev 27) @@ -1,9 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +## @file PyCodeOCR.py +# Turn your scanner into a free document +# reader for invoices +# (e.g. for e-banking) with the help of tesseract-ocr available +# for many unix (and also windows) platforms. - # python standard modules -import os, re, sys, subprocess, time, gobject +import os, re, sys, subprocess, time, gobject, string, struct # GTK, PyGTK, GLADE (GNOME) modules @@ -12,13 +16,15 @@ import gtk, gtk.glade import gobject +from checksum import modulo10 + # PIL image library and it's SANE bindings import Image, ImageDraw #from PIL import Image import sane # f12: 'python-imaging-sane' #(fedora 12 has no libWand, so it's not working at the moment...) -## ImageMagick's MagickWand API +# ImageMagick's MagickWand API #from pythonmagickwand.image import Image # search optional pakages @@ -26,6 +32,7 @@ os.environ["LD_LIBRARY_PATH"] = "/usr/local/lib" # os.system('python %s' % sys.argv[0]) # sys.exit() # +## list of available pakages pakages = [] # gocr if os.path.exists("gocr"): @@ -44,9 +51,13 @@ # global variables and constants +## path from which this program is currently executed local_path = os.path.realpath(os.path.dirname(sys.argv[0])) +## path of program without extension local_path/PyCodeOCR raw_path = os.path.join(local_path, os.path.splitext(sys.argv[0])[0]) +## users home path /home/user home_path = os.path.expanduser('~') +## default scanning coordinates std_scan_koords = { "0 deg": (( 82, 60, 20, 155 ), 0), # or with higher res. ( 820, 600, 200, 1559 ), "90 deg": (( 1, 82, 150, 20 ), 90), "180 deg": (( 3, 1, 20, 150 ), 180), @@ -54,7 +65,7 @@ "A4": (( 0, 0, 296, 215 ), 0), } # scan whole range (A4: http://www.cl.cam.ac.uk/~mgk25/iso-paper.html) #"A4": (( 0, 0, 290, 210 ), 0), } # (more secure; with small border) -# MainWindow +## MainWindow # The GUI was created/designed using GLADE # class MainWindowGTK: @@ -87,34 +98,64 @@ # (constants) MDE = { 'invoices': 0, 'barcode': 1, 'DataMatrix': 2, 'PDF417': 3 } + ## Initialize def __init__(self): - """ ... """ # retrieve widgets # window1 - main window + ## gladefile raw_path.glade self.gladefile = raw_path + ".glade" + ## get xml from gladefile self.xml = gtk.glade.XML(self.gladefile) + ## main window self.window1 = self.xml.get_widget('window1') + ## image window + self.window3 = self.xml.get_widget('window3') + ## scan button self.togglebutton1 = self.xml.get_widget('togglebutton1') + ## restart button self.button1 = self.xml.get_widget('button1') + ## exit button self.button2 = self.xml.get_widget('button2') + ## placement frame self.frame1 = self.xml.get_widget('frame1') + ## image self.image1 = self.xml.get_widget('image1') + ## image + self.image2 = self.xml.get_widget('image2') + ## output line self.entry1 = self.xml.get_widget('entry1') + ## progressbar self.progressbar1 = self.xml.get_widget('progressbar1') + ## Orientation label self.label5 = self.xml.get_widget('label5') + ## Orientation combobox self.combobox1 = self.xml.get_widget('combobox1') + ## mode combobox self.combobox2 = self.xml.get_widget('combobox2') + ## x-position self.spinbutton1 = self.xml.get_widget('spinbutton1') + ## y-position self.spinbutton2 = self.xml.get_widget('spinbutton2') + ## x-size self.spinbutton3 = self.xml.get_widget('spinbutton3') + ## y-size self.spinbutton4 = self.xml.get_widget('spinbutton4') + ## file input self.radiobutton1 = self.xml.get_widget('radiobutton1') + ## sane input self.radiobutton2 = self.xml.get_widget('radiobutton2') + ## choose file self.filechooserbutton1 = self.xml.get_widget('filechooserbutton1') - # window2 - sane + ## device select window self.window2 = self.xml.get_widget('window2') + ## device list self.treeview1 = self.xml.get_widget('treeview1') + ## OK button self.button3 = self.xml.get_widget('button3') + ## event box for image + self.eventbox2 = self.xml.get_widget('eventbox2') + ## event box for image window + self.eventbox1 = self.xml.get_widget('eventbox1') # initiate orientation and position self.combobox1.set_active(0) @@ -138,7 +179,14 @@ # connect signal handlers self.xml.signal_autoconnect( self ) + # connect eventbox to close image window + self.eventbox1.connect('button_press_event', self.on_eventbox2_button_press_event) + # set tooltips + self.button1.set_tooltip_text("Restart PyCodeOCR to search for a SANE device.") + self.image1.set_tooltip_text("Click for bigger image") + self.image2.set_tooltip_text("Click to close") + # take some time to init sane #self.init_sane() @@ -151,6 +199,7 @@ return + ## initialize sane def init_sane(self, source=None): # lock window self.window1.set_sensitive(False) @@ -171,22 +220,49 @@ return False + ## Run gtk mainloop and with it THIS APP. def run(self): - """ Run gtk mainloop and with it THIS APP. """ gtk.main() - # signals / glade callbacks + ## signals / glade callbacks # + def on_eventbox2_button_press_event(self, source=None, event=None): + if self.window3.get_visible(): + # clean-up + os.remove( self.image_file_big ) + # hide window + self.window3.hide() + else: + # load original image + im1 = Image.open(self.imageFile) + # adjust width and height to your needs and keep ratio + (im_width, im_height) = im1.size #get image size + (width, height) = self.image2.get_size_request() #get needed size + ratio = min(float(width)/im_width,float(height)/im_height) #calculate resizing ratio + new_width = int(im_width*ratio) + new_height = int(im_height*ratio) + print self.image2.get_size_request() + # resize to fit popup window + im = im1.resize((new_width, new_height), Image.BILINEAR) + # save + self.image_file_big = self.temp+"06.jpg" + im.save( self.image_file_big ) + # set image + self.image2.set_from_file( self.image_file_big ) + # show window + self.window3.show() + def on_button3_clicked(self, source=None): # get selection from treeview devnr = self.treeview1.get_cursor()[0][0] # hide window - self.window2.destroy() + self.window2.hide() # init scanner self.run_sane.init_scanner(self.progress, self.window2, self.treeview1, devnr) # set sensitivity self.init_sensitivity() + ## set sensitivity of buttons the first time def init_sensitivity(self): # set sensitivity if self.run_sane.found_scanner: @@ -195,7 +271,7 @@ else: self.radiobutton1.set_active(True) self.radiobutton2.set_sensitive(False) - self.on_radiobutton1_toggled() + #self.on_radiobutton1_toggled() self.progress(0.,"No SANE device found. Use file input instead.") self.button1.set_visible(True) @@ -205,14 +281,14 @@ # Try to set orientation and placement sensitive. Will fail if no device is found self.toggle_orientation_sensitive(True) self.toggle_placement_sensitive(True) - + + ## one of the placement coordinates changed def on_spinbutton_value_changed(self, source=None, event=None): - """ Spinbutton value changed signal handler. """ self.scan_koords = (self.spinbutton1.get_value(),self.spinbutton2.get_value(), self.spinbutton3.get_value(),self.spinbutton4.get_value()) + ## Orientation changed def on_combobox1_changed(self, source=None, event=None): - """ Combobox changed signal handler. """ orient = self.combobox1.get_model()[self.combobox1.get_active()][0] self.spinbutton1.set_value(std_scan_koords[orient][0][0]) @@ -222,8 +298,9 @@ self.mode = std_scan_koords[orient][1] + ## mode changed def on_combobox2_changed(self, source=None): - op_mode = self.combobox2.get_active() + op_mode = self.combobox2.get_active() if (op_mode == self.MDE['invoices']): self.toggle_placement_sensitive(True) self.toggle_orientation_sensitive(True) @@ -237,8 +314,8 @@ self.toggle_placement_sensitive(True) self.toggle_orientation_sensitive(True) + ## ToggleButton: 'scan'/'stop'. def on_togglebutton1_toggled(self, source=None, event=None, *a): - """ ToggleButton: 'scan'/'stop'. """ if self.togglebutton1.get_active(): # scan ! self.togglebutton1.set_label("stop") self.__stop = False @@ -266,48 +343,49 @@ self.refresh() return - + ## exit and restart def on_button1_clicked(self, source=None, event=None): - """ Button: 'exit'. """ os.execv(sys.argv[0],sys.argv) self.on_window1_destroy() - + + ## exit def on_button2_clicked(self, source=None, event=None): - """ Button: 'exit'. """ self.on_window1_destroy() - + + ## press x-button on window def on_window1_destroy(self, source=None, event=None): - """ Window closed signal handler. """ + try: + os.remove( self.temp+"04.jpg" ) # clean-up + os.remove( self.image_file_small ) + except: + pass + # exit gtk.main_quit() - + + ## press x-button on device select window def on_window2_destroy(self, source=None): # when windows is closed by pressing the x-button # choose first device - if source != None: - devnr = 0 - # init scanner - self.run_sane.init_scanner(self.progress, self.window2, self.treeview1, devnr) - # set sensitivity - self.init_sensitivity() + devnr = 0 + # init scanner + self.run_sane.init_scanner(self.progress, self.window2, self.treeview1, devnr) + # set sensitivity + self.init_sensitivity() - def on_radiobutton1_toggled(self, source=None, event=None): - #self.filechooserbutton1.set_sensitive( self.radiobutton1.get_active() ) - self.on_combobox2_changed() - - def on_radiobutton2_toggled(self, source=None, event=None): - self.on_combobox2_changed() - + ## file selected def on_filechooserbutton1_file_set(self, source=None): self.radiobutton1.set_active(True) self.inp_file = self.filechooserbutton1.get_filename() if not os.path.exists(self.inp_file): self.progress(0., "File not found!") return None - + + ## toggle orientation sensitivity def toggle_orientation_sensitive(self, value): self.label5.set_sensitive(value) self.combobox1.set_sensitive(value) - + + ## toggle placement sensitivity def toggle_placement_sensitive(self, value): if self.radiobutton2.get_active(): self.frame1.set_sensitive(value) @@ -315,9 +393,9 @@ self.frame1.set_sensitive(False) # helpers - # + # + ## Main scanning and number recognition procedure. def scancode(self): - """ Main scanning and number recognition procedure. """ # Initialization of scanning process # (0/7) self.progress(0., "") @@ -386,7 +464,7 @@ self.run_sane.post_init(self.scan_koords, self.inp_file) # RunSANE().post_init(...) 2nd part of __init__(...) self.run_sane.resolution = opt["resolution"] if self.run_sane(): - self.progress(0., self.run_sane.stderr[:-1]) + self.progress(0., self.run_sane.stderr) return None #del self.run_sane else: # direct file input @@ -487,6 +565,15 @@ if (op_mode == self.MDE['invoices']): # 0: invoices check = (not "?" in data) # any unrecognized char in code? check = check and ( len(data) in opt['valid_code_len'] ) # correct code len? + if len(data) == 42: + # extract details + (tmp, betrag, tmp, referenz, tmp, konto, tmp) = struct.unpack("2s11ss16s2s9ss",data) + print "Betrag: "+str(int(betrag[:-1])/100.) + print "Konto: "+konto[:2]+"-"+konto[3:-2]+"-"+konto[-2:] + print "Referenznr: "+referenz + # modulo10 checksum for betrag + checknr = modulo10().run(int(betrag[:-1])) + check = check and checknr == int(betrag[-1]) elif (op_mode == self.MDE['barcode']): # 1: barcode check = not (data['type'] == "unknown") if check: @@ -555,31 +642,37 @@ # (7/7) return check + ## Refresh window during running processes. def refresh(self): - """ Refresh window during running processes. """ self.window1.queue_draw() time.sleep(0.1) # give also some time to the user... :) while gtk.events_pending(): gtk.main_iteration() - def setimage(self, imageFile): - """ Resize and set PIL image to gtk/gnome window. """ - # http://www.daniweb.com/code/snippet216637.html - # resize an image using the PIL image library - # free from: http://www.pythonware.com/products/pil/index.htm - # tested with Python24 vegaseat 11oct2005 - # open an image file (.bmp,.jpg,.png,.gif) you have in the working folder + ## Resize and set PIL image to gtk/gnome window. + # resize an image using the PIL image library + # tested with Python24 vegaseat 11oct2005 + # open an image file (.bmp,.jpg,.png,.gif) you have in the working folder + # @see http://www.daniweb.com/code/snippet216637.html + # @see http://www.pythonware.com/products/pil/index.htm + def setimage(self, imageFile): print imageFile[-3:] if imageFile[-3:] in ["bmp","jpg","png","gif"]: #already in right format - im1 = Image.open(imageFile) + # original file name + self.imageFile = imageFile + # load file + im1 = Image.open(self.imageFile) else: #convert Image self.run_convert = RunExternal(self.cmd_convert % (imageFile, 0, self.temp+"04.jpg"), error_msg=[ "convert: unable to open image" ]) if self.run_convert(): self.progress(0., self.run_convert.stderr[:-1]) - return None - im1 = Image.open(self.temp+"04.jpg") + return None + # original filename + self.imageFile = self.temp+"04.jpg" + # load file + im1 = Image.open(self.imageFile) # adjust width and height to your needs and keep ratio (im_width, im_height) = im1.size #get image size @@ -596,19 +689,19 @@ #im = im1.resize((new_width, new_height), Image.ANTIALIAS) # best down-sizing filter #ext = ".jpg" - image_file = self.temp+"04.jpg" - im.save( image_file ) - self.image1.set_from_file( image_file ) - os.remove( image_file ) # clean-up + # set image preview + self.image_file_small = self.temp+"05.jpg" + im.save( self.image_file_small ) + self.image1.set_from_file( self.image_file_small ) + ## Set progress and refresh view. def progress(self, fract, text): - """ Set progress and refresh view. """ self.progressbar1.set_fraction(fract) self.progressbar1.set_text(text) self.refresh() + ## Character correction after recogition (on basis that there should be numbers and few special chars). def char_correction(self, data): - """ Character correction after recogition (on basis that there should be numbers and few special chars). """ data = re.sub("\n", "", data) print data corrections = [ @@ -662,22 +755,23 @@ return data - +## Run external shell command/application. class RunExternal: - """ Run external shell command/application. """ error_msg = [ "/bin/sh: " ] + ## initialize def __init__(self, cmd, error_msg): self.cmd = cmd self.error_msg += error_msg + ## call def __call__(self): (self.error, self.stdout, self.stderr) = self._run() return self.error + ## Execute external shell command. def _run(self, piped=True): - """ Execute external shell command. """ if piped: run = subprocess.Popen( self.cmd, stdin =subprocess.PIPE, @@ -773,16 +867,16 @@ # # return (False, "ok", "") +## sSANE/PIL interface python wrapper/bindings. class RunSANE: - """ SANE/PIL interface python wrapper/bindings. """ # "scanimage --format=tif --resolution 300 --mode Gray > %s.tif" # "scanimage --format=tif --resolution 600 --mode Gray -t 82 -y 20 -l 60 -x 155.9 > %s.tif" #cmd_scan = "scanimage --format=tif --resolution 600 --mode Gray -t %d -l %d -y %d -x %d > %s.tif" resolution = 600 + ## Init of sane interface -> device. def __init__(self, progress, window2, treeview): - """ Init of sane and scanner. """ print "init sane python interface ...", sys.stdout.flush() @@ -794,6 +888,9 @@ treeview.set_model(list_store) col = gtk.TreeViewColumn("Devices", gtk.CellRendererText(),text=0) treeview.append_column(col) + + # get blacklist + self.read_blacklist() # init SANE self.n=4. @@ -801,15 +898,28 @@ try: self.version = sane.init() progress(1./self.n,"init sane python interface ... search devices") - self.devices = sane.get_devices() + # get sane devices + self.devices1 = sane.get_devices() + # init filtered list + self.devices = [] + print "\n" # add found devices to treeview - for i in range(len(self.devices)): - print str(i)+": "+self.devices[i][1]+" "+self.devices[i][2] - list_store.append([self.devices[i][1]+" "+self.devices[i][2]]) + for i in range(len(self.devices1)): + # filter devices: remove blacklisted devices + if not self.devices1[i][1]+" "+self.devices1[i][2] in self.blacklist: + self.devices.append(self.devices1[i]) + print str(i)+": "+self.devices1[i][1]+" "+self.devices1[i][2] + list_store.append([self.devices1[i][1]+" "+self.devices1[i][2]]) + else: + print str(i)+": "+self.devices1[i][1]+" "+self.devices1[i][2]+" BLOCKED" + # self.devices is now filtered # check how many devices we found - if len(self.devices) > 1: + if len(self.devices) == 0: + progress(0./self.n,"No device found.") + self.found_scanner = False + elif len(list_store) > 1: # more than one device, choose one. progress(2./self.n,"More than one device found.") if sys.argv[1:]: @@ -832,12 +942,14 @@ # No device found at all self.found_scanner = False print "No sane device found. Restart to try it again." - + + ## iInit of sane interface -> scanner def init_scanner(self, progress, window2, treeview, devnr): - + print "Use device number: "+str(devnr) try: # finish init device - self.dev = sane.get_devices()[devnr][0] # choose first device + self.dev = self.devices[devnr][0] # choose first device + print self.dev progress(3./self.n,"Device initialized. Open scanner...") # open scanner device @@ -847,7 +959,7 @@ self.found_scanner = True - progress(4./self.n,"Ready.") + progress(4./self.n,"Ready: %s" % self.devices[devnr][1]+" "+self.devices[devnr][2]) print "done." except: @@ -856,20 +968,21 @@ print "Loading sane device failed. Restart to try it again." + ## Not init of sane and scanner, but of scan operation. def post_init(self, coords, out_filename): - """ Not init of sane and scanner, but of scan operation. """ self.coords = coords self.out_filename = out_filename self.info() + ## call def __call__(self): (self.error, self.stdout, self.stderr) = self._run() return self.error - # thanks to: http://mail.python.org/pipermail/image-sig/1997-June/000307.html - # (may be look also at: http://sane-pygtk.sourceforge.net/) + ## Scan and save the PIL image object to file. + # @see http://mail.python.org/pipermail/image-sig/1997-June/000307.html + # @see http://sane-pygtk.sourceforge.net/) def _run(self): - """ Scan and save the PIL image object to file. """ try: # Set scan parameters (also with higher resolution!) (t, l, y, x) = self.coords @@ -885,7 +998,7 @@ # Get an Image object containing the scanned image im = self.scanner.snap() - + # Write the image out as a TIFF file (or whatever) im.save(self.out_filename) @@ -895,8 +1008,8 @@ return (False, "ok", "") + ## Show some info about the scanner and SANE. def info(self): - """ Show some info about the scanner and SANE. """ print 'SANE version:', self.version print 'Available devices=', self.devices print 'Selected device=', self.dev #, "\n" @@ -905,10 +1018,24 @@ print 'Device parameters:', self.params #print 'Device options:', "\n",self.opts + ## get blacklisted devices + def read_blacklist(self): + bl = open('%s/blacklist' % local_path, 'r' ) + temp = bl.readlines() + bl.close + self.blacklist = [] + for line in temp: + self.blacklist.append(line.strip()) + ## write blacklisted devices + # @todo add UI way to blacklist device + def write_blacklist(self, blacklist): + bl = open('%s/blacklist' % local_path, 'w' ) + bl.write(blacklist) + bl.close() if __name__ == '__main__': - # run main application + ## run main application main = MainWindowGTK() main.run() Added: __init__.py =================================================================== Added: blacklist =================================================================== --- blacklist (rev 0) +++ blacklist 2010-10-25 18:39:31 UTC (rev 27) @@ -0,0 +1 @@ +Printer Name Added: checksum.py =================================================================== --- checksum.py (rev 0) +++ checksum.py 2010-10-25 18:39:31 UTC (rev 27) @@ -0,0 +1,19 @@ +## @file checksum.py +# Checksum for 13 digit e-banking codes +class modulo10: + # thanks to http://www.hosang.ch/modulo10.aspx + # much more details http://www.sic.ch/de/dl_tkicch_dta.pdf ( page 51 ) + def __init__(self): + self.tabelle = [0,9,4,6,8,2,7,1,3,5] + self.uebertrag = 0 + + def run(self,number): + # make string + nr = str(number) + # iterate over each character + for i in range(len(nr)): + self.uebertrag = self.tabelle[(self.uebertrag + int(nr[i]))%10] + + return self.uebertrag + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |