qidenticon.py
1 ### 2 # qidenticon.py is Licesensed under FreeBSD License. 3 # (http://www.freebsd.org/copyright/freebsd-license.html) 4 # 5 # Copyright 1994-2009 Shin Adachi. All rights reserved. 6 # Copyright 2013 "Sendiulo". All rights reserved. 7 # Copyright 2018-2021 The Bitmessage Developers. All rights reserved. 8 # 9 # Redistribution and use in source and binary forms, 10 # with or without modification, are permitted provided that the following 11 # conditions are met: 12 # 13 # 1. Redistributions of source code must retain the above copyright notice, 14 # this list of conditions and the following disclaimer. 15 # 2. Redistributions in binary form must reproduce the above copyright 16 # notice, this list of conditions and the following disclaimer in the 17 # documentation and/or other materials provided with the distribution. 18 # 19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 20 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 23 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 ### 30 31 # pylint: disable=too-many-locals,too-many-arguments,too-many-function-args 32 """ 33 Usage 34 ----- 35 36 >>> import qidenticon 37 >>> qidenticon.render_identicon(code, size) 38 39 Returns an instance of :class:`QPixmap` which have generated identicon image. 40 ``size`` specifies `patch size`. Generated image size is 3 * ``size``. 41 """ 42 43 from six.moves import range 44 45 try: 46 from PyQt5 import QtCore, QtGui 47 except (ImportError, RuntimeError): 48 from PyQt4 import QtCore, QtGui 49 50 51 class IdenticonRendererBase(object): 52 """Encapsulate methods around rendering identicons""" 53 54 PATH_SET = [] 55 56 def __init__(self, code): 57 """ 58 :param code: code for icon 59 """ 60 if not isinstance(code, int): 61 code = int(code) 62 self.code = code 63 64 def render(self, size, twoColor, opacity, penwidth): 65 """ 66 render identicon to QPixmap 67 68 :param size: identicon patchsize. (image size is 3 * [size]) 69 :returns: :class:`QPixmap` 70 """ 71 72 # decode the code 73 middle, corner, side, foreColor, secondColor, swap_cross = \ 74 self.decode(self.code, twoColor) 75 76 # make image 77 image = QtGui.QPixmap( 78 QtCore.QSize(size * 3 + penwidth, size * 3 + penwidth)) 79 80 # fill background 81 backColor = QtGui.QColor(255, 255, 255, opacity) 82 image.fill(backColor) 83 84 kwds = { 85 'image': image, 86 'size': size, 87 'foreColor': foreColor if swap_cross else secondColor, 88 'penwidth': penwidth, 89 'backColor': backColor} 90 91 # middle patch 92 image = self.drawPatchQt( 93 (1, 1), middle[2], middle[1], middle[0], **kwds) 94 95 # side patch 96 kwds['foreColor'] = foreColor 97 kwds['patch_type'] = side[0] 98 for i in range(4): 99 pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i] 100 image = self.drawPatchQt(pos, side[2] + 1 + i, side[1], **kwds) 101 102 # corner patch 103 kwds['foreColor'] = secondColor 104 kwds['patch_type'] = corner[0] 105 for i in range(4): 106 pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i] 107 image = self.drawPatchQt(pos, corner[2] + 1 + i, corner[1], **kwds) 108 109 return image 110 111 def drawPatchQt( 112 self, pos, turn, invert, patch_type, image, size, foreColor, 113 backColor, penwidth): # pylint: disable=unused-argument 114 """ 115 :param size: patch size 116 """ 117 path = self.PATH_SET[patch_type] 118 if not path: 119 # blank patch 120 invert = not invert 121 path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)] 122 123 polygon = QtGui.QPolygonF([ 124 QtCore.QPointF(x * size, y * size) for x, y in path]) 125 126 rot = turn % 4 127 rect = [ 128 QtCore.QPointF(0., 0.), QtCore.QPointF(size, 0.), 129 QtCore.QPointF(size, size), QtCore.QPointF(0., size)] 130 rotation = [0, 90, 180, 270] 131 132 nopen = QtGui.QPen(foreColor, QtCore.Qt.NoPen) 133 foreBrush = QtGui.QBrush(foreColor, QtCore.Qt.SolidPattern) 134 if penwidth > 0: 135 pen_color = QtGui.QColor(255, 255, 255) 136 pen = QtGui.QPen(pen_color, QtCore.Qt.SolidPattern) 137 pen.setWidth(penwidth) 138 139 painter = QtGui.QPainter() 140 painter.begin(image) 141 painter.setPen(nopen) 142 143 painter.translate( 144 pos[0] * size + penwidth / 2, pos[1] * size + penwidth / 2) 145 painter.translate(rect[rot]) 146 painter.rotate(rotation[rot]) 147 148 if invert: 149 # subtract the actual polygon from a rectangle to invert it 150 poly_rect = QtGui.QPolygonF(rect) 151 polygon = poly_rect.subtracted(polygon) 152 painter.setBrush(foreBrush) 153 if penwidth > 0: 154 # draw the borders 155 painter.setPen(pen) 156 painter.drawPolygon(polygon, QtCore.Qt.WindingFill) 157 # draw the fill 158 painter.setPen(nopen) 159 painter.drawPolygon(polygon, QtCore.Qt.WindingFill) 160 161 painter.end() 162 163 return image 164 165 def decode(self, code, twoColor): 166 """virtual functions""" 167 raise NotImplementedError 168 169 170 class DonRenderer(IdenticonRendererBase): 171 """ 172 Don Park's implementation of identicon, see: 173 https://blog.docuverse.com/2007/01/18/identicon-updated-and-source-released 174 """ 175 176 PATH_SET = [ 177 # [0] full square: 178 [(0, 0), (4, 0), (4, 4), (0, 4)], 179 # [1] right-angled triangle pointing top-left: 180 [(0, 0), (4, 0), (0, 4)], 181 # [2] upwardy triangle: 182 [(2, 0), (4, 4), (0, 4)], 183 # [3] left half of square, standing rectangle: 184 [(0, 0), (2, 0), (2, 4), (0, 4)], 185 # [4] square standing on diagonale: 186 [(2, 0), (4, 2), (2, 4), (0, 2)], 187 # [5] kite pointing topleft: 188 [(0, 0), (4, 2), (4, 4), (2, 4)], 189 # [6] Sierpinski triangle, fractal triangles: 190 [(2, 0), (4, 4), (2, 4), (3, 2), (1, 2), (2, 4), (0, 4)], 191 # [7] sharp angled lefttop pointing triangle: 192 [(0, 0), (4, 2), (2, 4)], 193 # [8] small centered square: 194 [(1, 1), (3, 1), (3, 3), (1, 3)], 195 # [9] two small triangles: 196 [(2, 0), (4, 0), (0, 4), (0, 2), (2, 2)], 197 # [10] small topleft square: 198 [(0, 0), (2, 0), (2, 2), (0, 2)], 199 # [11] downpointing right-angled triangle on bottom: 200 [(0, 2), (4, 2), (2, 4)], 201 # [12] uppointing right-angled triangle on bottom: 202 [(2, 2), (4, 4), (0, 4)], 203 # [13] small rightbottom pointing right-angled triangle on topleft: 204 [(2, 0), (2, 2), (0, 2)], 205 # [14] small lefttop pointing right-angled triangle on topleft: 206 [(0, 0), (2, 0), (0, 2)], 207 # [15] empty: 208 []] 209 # get the [0] full square, [4] square standing on diagonale, 210 # [8] small centered square, or [15] empty tile: 211 MIDDLE_PATCH_SET = [0, 4, 8, 15] 212 213 # modify path set 214 for idx, path in enumerate(PATH_SET): 215 if path: 216 p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in path] 217 PATH_SET[idx] = p + p[:1] 218 219 def decode(self, code, twoColor): 220 """decode the code""" 221 222 shift = 0 223 middleType = (code >> shift) & 0x03 224 shift += 2 225 middleInvert = (code >> shift) & 0x01 226 shift += 1 227 cornerType = (code >> shift) & 0x0F 228 shift += 4 229 cornerInvert = (code >> shift) & 0x01 230 shift += 1 231 cornerTurn = (code >> shift) & 0x03 232 shift += 2 233 sideType = (code >> shift) & 0x0F 234 shift += 4 235 sideInvert = (code >> shift) & 0x01 236 shift += 1 237 sideTurn = (code >> shift) & 0x03 238 shift += 2 239 blue = (code >> shift) & 0x1F 240 shift += 5 241 green = (code >> shift) & 0x1F 242 shift += 5 243 red = (code >> shift) & 0x1F 244 shift += 5 245 second_blue = (code >> shift) & 0x1F 246 shift += 5 247 second_green = (code >> shift) & 0x1F 248 shift += 5 249 second_red = (code >> shift) & 0x1F 250 shift += 1 251 swap_cross = (code >> shift) & 0x01 252 253 middleType = self.MIDDLE_PATCH_SET[middleType] 254 255 foreColor = (red << 3, green << 3, blue << 3) 256 foreColor = QtGui.QColor(*foreColor) 257 258 if twoColor: 259 secondColor = ( 260 second_blue << 3, second_green << 3, second_red << 3) 261 secondColor = QtGui.QColor(*secondColor) 262 else: 263 secondColor = foreColor 264 265 return (middleType, middleInvert, 0),\ 266 (cornerType, cornerInvert, cornerTurn),\ 267 (sideType, sideInvert, sideTurn),\ 268 foreColor, secondColor, swap_cross 269 270 271 def render_identicon( 272 code, size, twoColor=False, opacity=255, penwidth=0, renderer=None): 273 """Render an image""" 274 if not renderer: 275 renderer = DonRenderer 276 return renderer(code).render(size, twoColor, opacity, penwidth)