Polish the qidenticon #1793

Merged
g1itch merged 4 commits from qidenticon into v0.6 2021-08-26 15:43:08 +02:00
3 changed files with 125 additions and 35 deletions

View File

@ -3,3 +3,4 @@ python_prctl
psutil psutil
pycrypto pycrypto
six six
PyQt5;python_version>="3.7"

View File

@ -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 # pylint: disable=too-many-locals,too-many-arguments,too-many-function-args
""" """
Usage Usage
----- -----
>>> import qtidenticon >>> import qidenticon
>>> qtidenticon.render_identicon(code, size) >>> 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``. ``size`` specifies `patch size`. Generated image size is 3 * ``size``.
""" """
from PyQt4 import QtGui try:
from PyQt4.QtCore import QPointF, QSize, Qt from PyQt5 import QtCore, QtGui
from PyQt4.QtGui import QPainter, QPixmap, QPolygonF except ImportError:
from PyQt4 import QtCore, QtGui
class IdenticonRendererBase(object): class IdenticonRendererBase(object):
@ -30,17 +61,19 @@ class IdenticonRendererBase(object):
def render(self, size, twoColor, opacity, penwidth): def render(self, size, twoColor, opacity, penwidth):
""" """
render identicon to QPicture render identicon to QPixmap
:param size: identicon patchsize. (image size is 3 * [size]) :param size: identicon patchsize. (image size is 3 * [size])
:returns: :class:`QPicture` :returns: :class:`QPixmap`
""" """
# decode the code # 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 # make image
image = QPixmap(QSize(size * 3 + penwidth, size * 3 + penwidth)) image = QtGui.QPixmap(
QtCore.QSize(size * 3 + penwidth, size * 3 + penwidth))
# fill background # fill background
backColor = QtGui.QColor(255, 255, 255, opacity) backColor = QtGui.QColor(255, 255, 255, opacity)
@ -54,26 +87,28 @@ class IdenticonRendererBase(object):
'backColor': backColor} 'backColor': backColor}
# middle patch # 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 # side patch
kwds['foreColor'] = foreColor kwds['foreColor'] = foreColor
kwds['patch_type'] = side[0] 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] pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i]
image = self.drawPatchQt(pos, side[2] + 1 + i, side[1], **kwds) image = self.drawPatchQt(pos, side[2] + 1 + i, side[1], **kwds)
# corner patch # corner patch
kwds['foreColor'] = secondColor kwds['foreColor'] = secondColor
kwds['patch_type'] = corner[0] 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] pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i]
image = self.drawPatchQt(pos, corner[2] + 1 + i, corner[1], **kwds) image = self.drawPatchQt(pos, corner[2] + 1 + i, corner[1], **kwds)
return image return image
def drawPatchQt(self, pos, turn, invert, patch_type, image, size, foreColor, def drawPatchQt(
backColor, penwidth): # pylint: disable=unused-argument self, pos, turn, invert, patch_type, image, size, foreColor,
backColor, penwidth): # pylint: disable=unused-argument
""" """
:param size: patch size :param size: patch size
""" """
@ -83,39 +118,43 @@ class IdenticonRendererBase(object):
invert = not invert invert = not invert
path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)] 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 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] rotation = [0, 90, 180, 270]
nopen = QtGui.QPen(foreColor, Qt.NoPen) nopen = QtGui.QPen(foreColor, QtCore.Qt.NoPen)
foreBrush = QtGui.QBrush(foreColor, Qt.SolidPattern) foreBrush = QtGui.QBrush(foreColor, QtCore.Qt.SolidPattern)
if penwidth > 0: if penwidth > 0:
pen_color = QtGui.QColor(255, 255, 255) 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) pen.setWidth(penwidth)
painter = QPainter() painter = QtGui.QPainter()
painter.begin(image) painter.begin(image)
painter.setPen(nopen) 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.translate(rect[rot])
painter.rotate(rotation[rot]) painter.rotate(rotation[rot])
if invert: if invert:
# subtract the actual polygon from a rectangle to invert it # subtract the actual polygon from a rectangle to invert it
poly_rect = QPolygonF(rect) poly_rect = QtGui.QPolygonF(rect)
polygon = poly_rect.subtracted(polygon) polygon = poly_rect.subtracted(polygon)
painter.setBrush(foreBrush) painter.setBrush(foreBrush)
if penwidth > 0: if penwidth > 0:
# draw the borders # draw the borders
painter.setPen(pen) painter.setPen(pen)
painter.drawPolygon(polygon, Qt.WindingFill) painter.drawPolygon(polygon, QtCore.Qt.WindingFill)
# draw the fill # draw the fill
painter.setPen(nopen) painter.setPen(nopen)
painter.drawPolygon(polygon, Qt.WindingFill) painter.drawPolygon(polygon, QtCore.Qt.WindingFill)
painter.end() painter.end()
@ -123,14 +162,13 @@ class IdenticonRendererBase(object):
def decode(self, code, twoColor): def decode(self, code, twoColor):
"""virtual functions""" """virtual functions"""
raise NotImplementedError raise NotImplementedError
class DonRenderer(IdenticonRendererBase): class DonRenderer(IdenticonRendererBase):
""" """
Don Park's implementation of identicon Don Park's implementation of identicon, see:
see: http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released https://blog.docuverse.com/2007/01/18/identicon-updated-and-source-released
""" """
PATH_SET = [ PATH_SET = [
@ -166,13 +204,14 @@ class DonRenderer(IdenticonRendererBase):
[(0, 0), (2, 0), (0, 2)], [(0, 0), (2, 0), (0, 2)],
# [15] empty: # [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] MIDDLE_PATCH_SET = [0, 4, 8, 15]
# modify path set # modify path set
for idx in xrange(len(PATH_SET)): for idx, path in enumerate(PATH_SET):
if PATH_SET[idx]: if path:
p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in PATH_SET[idx]] p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in path]
PATH_SET[idx] = p + p[:1] PATH_SET[idx] = p + p[:1]
def decode(self, code, twoColor): def decode(self, code, twoColor):
@ -215,7 +254,8 @@ class DonRenderer(IdenticonRendererBase):
foreColor = QtGui.QColor(*foreColor) foreColor = QtGui.QColor(*foreColor)
if twoColor: 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) secondColor = QtGui.QColor(*secondColor)
else: else:
secondColor = foreColor secondColor = foreColor
@ -226,9 +266,9 @@ class DonRenderer(IdenticonRendererBase):
foreColor, secondColor, swap_cross 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""" """Render an image"""
if not renderer: if not renderer:
renderer = DonRenderer renderer = DonRenderer
return renderer(code).render(size, twoColor, opacity, penwidth) return renderer(code).render(size, twoColor, opacity, penwidth)

View File

@ -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())