/ src / qidenticon.py
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)