diff --git a/requirements.txt b/requirements.txt index 0e1322d4..b12641c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ python_prctl psutil pycrypto six +PyQt5;python_version>="3.7" diff --git a/src/qidenticon.py b/src/qidenticon.py index 6eab09cd..b8e92481 100644 --- a/src/qidenticon.py +++ b/src/qidenticon.py @@ -1,18 +1,49 @@ +### +# qidenticon.py is Licesensed under FreeBSD License. +# (http://www.freebsd.org/copyright/freebsd-license.html) +# +# Copyright 1994-2009 Shin Adachi. All rights reserved. +# Copyright 2013 "Sendiulo". All rights reserved. +# Copyright 2018-2021 The Bitmessage Developers. All rights reserved. +# +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +### + # pylint: disable=too-many-locals,too-many-arguments,too-many-function-args """ Usage ----- ->>> import qtidenticon ->>> qtidenticon.render_identicon(code, size) +>>> import qidenticon +>>> qidenticon.render_identicon(code, size) -Return a PIL Image class instance which have generated identicon image. +Returns an instance of :class:`QPixmap` which have generated identicon image. ``size`` specifies `patch size`. Generated image size is 3 * ``size``. """ -from PyQt4 import QtGui -from PyQt4.QtCore import QPointF, QSize, Qt -from PyQt4.QtGui import QPainter, QPixmap, QPolygonF +try: + from PyQt5 import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui class IdenticonRendererBase(object): @@ -30,17 +61,19 @@ class IdenticonRendererBase(object): def render(self, size, twoColor, opacity, penwidth): """ - render identicon to QPicture + render identicon to QPixmap :param size: identicon patchsize. (image size is 3 * [size]) - :returns: :class:`QPicture` + :returns: :class:`QPixmap` """ # decode the code - middle, corner, side, foreColor, secondColor, swap_cross = self.decode(self.code, twoColor) + middle, corner, side, foreColor, secondColor, swap_cross = \ + self.decode(self.code, twoColor) # make image - image = QPixmap(QSize(size * 3 + penwidth, size * 3 + penwidth)) + image = QtGui.QPixmap( + QtCore.QSize(size * 3 + penwidth, size * 3 + penwidth)) # fill background backColor = QtGui.QColor(255, 255, 255, opacity) @@ -54,26 +87,28 @@ class IdenticonRendererBase(object): 'backColor': backColor} # middle patch - image = self.drawPatchQt((1, 1), middle[2], middle[1], middle[0], **kwds) + image = self.drawPatchQt( + (1, 1), middle[2], middle[1], middle[0], **kwds) # side patch kwds['foreColor'] = foreColor kwds['patch_type'] = side[0] - for i in xrange(4): + for i in range(4): pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i] image = self.drawPatchQt(pos, side[2] + 1 + i, side[1], **kwds) # corner patch kwds['foreColor'] = secondColor kwds['patch_type'] = corner[0] - for i in xrange(4): + for i in range(4): pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i] image = self.drawPatchQt(pos, corner[2] + 1 + i, corner[1], **kwds) return image - def drawPatchQt(self, pos, turn, invert, patch_type, image, size, foreColor, - backColor, penwidth): # pylint: disable=unused-argument + def drawPatchQt( + self, pos, turn, invert, patch_type, image, size, foreColor, + backColor, penwidth): # pylint: disable=unused-argument """ :param size: patch size """ @@ -83,39 +118,43 @@ class IdenticonRendererBase(object): invert = not invert path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)] - polygon = QPolygonF([QPointF(x * size, y * size) for x, y in path]) + polygon = QtGui.QPolygonF([ + QtCore.QPointF(x * size, y * size) for x, y in path]) rot = turn % 4 - rect = [QPointF(0., 0.), QPointF(size, 0.), QPointF(size, size), QPointF(0., size)] + rect = [ + QtCore.QPointF(0., 0.), QtCore.QPointF(size, 0.), + QtCore.QPointF(size, size), QtCore.QPointF(0., size)] rotation = [0, 90, 180, 270] - nopen = QtGui.QPen(foreColor, Qt.NoPen) - foreBrush = QtGui.QBrush(foreColor, Qt.SolidPattern) + nopen = QtGui.QPen(foreColor, QtCore.Qt.NoPen) + foreBrush = QtGui.QBrush(foreColor, QtCore.Qt.SolidPattern) if penwidth > 0: pen_color = QtGui.QColor(255, 255, 255) - pen = QtGui.QPen(pen_color, Qt.SolidPattern) + pen = QtGui.QPen(pen_color, QtCore.Qt.SolidPattern) pen.setWidth(penwidth) - painter = QPainter() + painter = QtGui.QPainter() painter.begin(image) painter.setPen(nopen) - painter.translate(pos[0] * size + penwidth / 2, pos[1] * size + penwidth / 2) + painter.translate( + pos[0] * size + penwidth / 2, pos[1] * size + penwidth / 2) painter.translate(rect[rot]) painter.rotate(rotation[rot]) if invert: # subtract the actual polygon from a rectangle to invert it - poly_rect = QPolygonF(rect) + poly_rect = QtGui.QPolygonF(rect) polygon = poly_rect.subtracted(polygon) painter.setBrush(foreBrush) if penwidth > 0: # draw the borders painter.setPen(pen) - painter.drawPolygon(polygon, Qt.WindingFill) + painter.drawPolygon(polygon, QtCore.Qt.WindingFill) # draw the fill painter.setPen(nopen) - painter.drawPolygon(polygon, Qt.WindingFill) + painter.drawPolygon(polygon, QtCore.Qt.WindingFill) painter.end() @@ -123,14 +162,13 @@ class IdenticonRendererBase(object): def decode(self, code, twoColor): """virtual functions""" - raise NotImplementedError class DonRenderer(IdenticonRendererBase): """ - Don Park's implementation of identicon - see: http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released + Don Park's implementation of identicon, see: + https://blog.docuverse.com/2007/01/18/identicon-updated-and-source-released """ PATH_SET = [ @@ -166,13 +204,14 @@ class DonRenderer(IdenticonRendererBase): [(0, 0), (2, 0), (0, 2)], # [15] empty: []] - # get the [0] full square, [4] square standing on diagonale, [8] small centered square, or [15] empty tile: + # get the [0] full square, [4] square standing on diagonale, + # [8] small centered square, or [15] empty tile: MIDDLE_PATCH_SET = [0, 4, 8, 15] # modify path set - for idx in xrange(len(PATH_SET)): - if PATH_SET[idx]: - p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in PATH_SET[idx]] + for idx, path in enumerate(PATH_SET): + if path: + p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in path] PATH_SET[idx] = p + p[:1] def decode(self, code, twoColor): @@ -215,7 +254,8 @@ class DonRenderer(IdenticonRendererBase): foreColor = QtGui.QColor(*foreColor) if twoColor: - secondColor = (second_blue << 3, second_green << 3, second_red << 3) + secondColor = ( + second_blue << 3, second_green << 3, second_red << 3) secondColor = QtGui.QColor(*secondColor) else: secondColor = foreColor @@ -226,9 +266,9 @@ class DonRenderer(IdenticonRendererBase): foreColor, secondColor, swap_cross -def render_identicon(code, size, twoColor=False, opacity=255, penwidth=0, renderer=None): +def render_identicon( + code, size, twoColor=False, opacity=255, penwidth=0, renderer=None): """Render an image""" - if not renderer: renderer = DonRenderer return renderer(code).render(size, twoColor, opacity, penwidth) diff --git a/src/tests/test_identicon.py b/src/tests/test_identicon.py new file mode 100644 index 00000000..4c6be32d --- /dev/null +++ b/src/tests/test_identicon.py @@ -0,0 +1,49 @@ +"""Tests for qidenticon""" + +import atexit +import unittest + +try: + from PyQt5 import QtGui, QtWidgets + from xvfbwrapper import Xvfb + from pybitmessage import qidenticon +except ImportError: + Xvfb = None + # raise unittest.SkipTest( + # 'Skipping graphical test, because of no PyQt or xvfbwrapper') +else: + vdisplay = Xvfb(width=1024, height=768) + vdisplay.start() + atexit.register(vdisplay.stop) + + +sample_code = 0x3fd4bf901b9d4ea1394f0fb358725b28 +sample_size = 48 + + +@unittest.skipUnless( + Xvfb, 'Skipping graphical test, because of no PyQt or xvfbwrapper') +class TestIdenticon(unittest.TestCase): + """QIdenticon implementation test case""" + + @classmethod + def setUpClass(cls): + """Instantiate QtWidgets.QApplication""" + cls.app = QtWidgets.QApplication([]) + + def test_qidenticon_samples(self): + """Generate 4 qidenticon samples and check their properties""" + icon_simple = qidenticon.render_identicon(sample_code, sample_size) + self.assertIsInstance(icon_simple, QtGui.QPixmap) + self.assertEqual(icon_simple.height(), sample_size * 3) + self.assertEqual(icon_simple.width(), sample_size * 3) + self.assertFalse(icon_simple.hasAlphaChannel()) + + # icon_sample = QtGui.QPixmap() + # icon_sample.load('../images/qidenticon.png') + # self.assertFalse( + # icon_simple.toImage(), icon_sample.toImage()) + + icon_x = qidenticon.render_identicon( + sample_code, sample_size, opacity=0) + self.assertTrue(icon_x.hasAlphaChannel())