Polish the qidenticon #1793
|
@ -3,3 +3,4 @@ python_prctl
|
||||||
psutil
|
psutil
|
||||||
pycrypto
|
pycrypto
|
||||||
six
|
six
|
||||||
|
PyQt5;python_version>="3.7"
|
||||||
|
|
|
@ -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,25 +87,27 @@ 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(
|
||||||
|
self, pos, turn, invert, patch_type, image, size, foreColor,
|
||||||
backColor, penwidth): # pylint: disable=unused-argument
|
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)
|
||||||
|
|
49
src/tests/test_identicon.py
Normal file
49
src/tests/test_identicon.py
Normal 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())
|
Reference in New Issue
Block a user