# -*- coding: utf-8 -*- # Copyright(C) 2011 Pierre Mazière # # This file is part of weboob. # # weboob is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # weboob is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . import hashlib import tempfile from string import maketrans try: from PIL import Image except ImportError: raise ImportError('Please install python-imaging') class VirtKeyboardError(Exception): pass class VirtKeyboard(object): """ Handle a virtual keyboard. :attribute margin: Margin used by :meth:`get_symbol_coords̀` to reduce size of each "key" of the virtual keyboard. This attribute is always converted to a 4-tuple, and has the same semantic as the CSS ``margin`` property (top, right, bottom, right), in pixels. :type margin: int or float or (2|3|4)-tuple """ margin = None def __init__(self, file=None, coords=None, color=None, convert=None): # file: virtual keyboard image # coords: dictionary : # color: color of the symbols in the image # depending on the image, it can be a single value or a tuple # convert: if not None, convert image to this target type (for example 'RGB') if file is not None: assert color, 'No color provided !' self.load_image(file, color, convert) if type(self.margin) in (int, float): self.margin = (self.margin,) * 4 elif self.margin is not None: if len(self.margin) == 2: self.margin = self.margin + self.margin elif len(self.margin) == 3: self.margin = self.margin + (self.margin[1],) assert len(self.margin) == 4 if coords is not None: self.load_symbols(coords) def load_image(self, file, color, convert=None): self.image = Image.open(file) if convert is not None: self.image = self.image.convert(convert) self.bands = self.image.getbands() if isinstance(color, int) and not isinstance(self.bands, str) and len(self.bands) != 1: raise VirtKeyboardError("Color requires %i component but only 1 is provided" % len(self.bands)) if not isinstance(color, int) and len(color) != len(self.bands): raise VirtKeyboardError("Color requires %i components but %i are provided" % (len(self.bands), len(color))) self.color = color self.width, self.height = self.image.size self.pixar = self.image.load() def load_symbols(self, coords): self.coords = {} self.md5 = {} for i in coords: coord = self.get_symbol_coords(coords[i]) if coord == (-1, -1, -1, -1): continue self.coords[i] = coord self.md5[i] = self.checksum(self.coords[i]) def check_color(self, pixel): return pixel == self.color def get_symbol_coords(self, coords): (x1, y1, x2, y2) = coords if self.margin: top, right, bottom, left = self.margin x1, y1, x2, y2 = x1 + left, y1 + top, x2 - right, y2 - bottom newY1 = -1 newY2 = -1 for y in range(y1, min(y2 + 1, self.height)): empty_line = True for x in range(x1, min(x2 + 1, self.width)): if self.check_color(self.pixar[x, y]): empty_line = False if newY1 < 0: newY1 = y break if newY1 >= 0 and not empty_line: newY2 = y newX1 = -1 newX2 = -1 for x in range(x1, min(x2 + 1, self.width)): empty_column = True for y in range(y1, min(y2 + 1, self.height)): if self.check_color(self.pixar[x, y]): empty_column = False if newX1 < 0: newX1 = x break if newX1 >= 0 and not empty_column: newX2 = x return (newX1, newY1, newX2, newY2) def checksum(self, coords): (x1, y1, x2, y2) = coords s = '' for y in range(y1, min(y2 + 1, self.height)): for x in range(x1, min(x2 + 1, self.width)): if self.check_color(self.pixar[x, y]): s += "." else: s += " " return hashlib.md5(s).hexdigest() def get_symbol_code(self, md5sum): for i in self.md5: if md5sum == self.md5[i]: return i raise VirtKeyboardError('Symbol not found') def check_symbols(self, symbols, dirname): # symbols: dictionary : for s in symbols: try: self.get_symbol_code(symbols[s]) except VirtKeyboardError: if dirname is None: dirname = tempfile.mkdtemp(prefix='weboob_session_') self.generate_MD5(dirname) raise VirtKeyboardError("Symbol '%s' not found; all symbol hashes are available in %s" % (s, dirname)) def generate_MD5(self, dir): for i in self.coords: width = self.coords[i][2] - self.coords[i][0] + 1 height = self.coords[i][3] - self.coords[i][1] + 1 img = Image.new(''.join(self.bands), (width, height)) matrix = img.load() for y in range(height): for x in range(width): matrix[x, y] = self.pixar[self.coords[i][0] + x, self.coords[i][1] + y] img.save(dir + "/" + self.md5[i] + ".png") self.image.save(dir + "/image.png") class MappedVirtKeyboard(VirtKeyboard): def __init__(self, file, document, img_element, color, map_attr="onclick", convert=None): map_id = img_element.attrib.get("usemap")[1:] map = document.find("//map[@id='" + map_id + "']") if map is None: map = document.find("//map[@name='" + map_id + "']") coords = {} for area in map.getiterator("area"): code = area.attrib.get(map_attr) area_coords = [] for coord in area.attrib.get("coords").split(','): area_coords.append(int(coord)) coords[code] = tuple(area_coords) super(MappedVirtKeyboard, self).__init__(file, coords, color, convert) class GridVirtKeyboard(VirtKeyboard): """ Make a virtual keyboard where "keys" are distributed on a grid. For example: https://www.esgbl.com/part/fr/idehom.html Parameters: :param symbols: Sequence of symbols, ordered in the grid from left to right and up to down :type symbols: iterable :param cols: Column count of the grid :type cols: int :param rows: Row count of the grid :type rows: int :param image: File-like object to be used as data source :type image: file :param color: Color of the meaningful pixels :type color: 3-tuple :param convert: Mode to which convert color of pixels, see :meth:`Image.Image.convert` for more information Attributes: :attribute symbols: Association table between symbols and md5s :type symbols: dict """ symbols = {} def __init__(self, symbols, cols, rows, image, color, convert=None): self.load_image(image, color, convert) tileW = float(self.width) / cols tileH = float(self.height) / rows positions = ((s, i * tileW % self.width, i / cols * tileH) for i, s in enumerate(symbols)) coords = dict((s, tuple(map(int, (x, y, x + tileW, y + tileH)))) for (s, x, y) in positions) super(GridVirtKeyboard, self).__init__() self.load_symbols(coords) def load_symbols(self, coords): super(GridVirtKeyboard, self).load_symbols(coords) symbol_codes = map(self.get_symbol_code, self.symbols.itervalues()) self._trans = maketrans(''.join(self.symbols), ''.join(symbol_codes)) def get_string_code(self, string): return str(string).translate(self._trans)