233 lines
10 KiB
Python
233 lines
10 KiB
Python
# Needed on case-insensitive filesystems
|
|
from __future__ import absolute_import
|
|
|
|
# Try to import PIL in either of the two ways it can be installed.
|
|
try:
|
|
from PIL import Image, ImageDraw
|
|
except ImportError: # pragma: no cover
|
|
import Image
|
|
import ImageDraw
|
|
|
|
# When drawing antialiased things, make them bigger and then shrink them down to size after the geometry has been drawn
|
|
ANTIALIASING_FACTOR = 4
|
|
|
|
class QRModuleDrawer:
|
|
"""
|
|
QRModuleDrawer exists to draw the modules of the QR Code onto a PIL image
|
|
For this, technically all that is necessary is a drawrect_context(self, box, is_active, context) function
|
|
which takes in the box in which it is to draw, whether or not the box is "active" (a module exists there),
|
|
and the context (the neighboring pixels)
|
|
It is frequently necessary to also implement an "initialize" function to set up values that only the containing StyledPilImage
|
|
knows about.
|
|
NOTE: the color that this draws in should be whatever is equivalent to black in the color space, and the specified QRColorMask
|
|
will handle adding colors as necessary to the image
|
|
|
|
For examples of what these look like, see doc/module_drawers.png
|
|
"""
|
|
|
|
fill = None
|
|
def initialize(self, styledPilImage, image):
|
|
self.fill = styledPilImage.paint_color
|
|
|
|
def drawrect_context(self, box, is_active, context):
|
|
raise NotImplementedError("QRModuleDrawer.drawrect_context")
|
|
|
|
# helper for figuring out the context, which is an array containing information on neighboring pixels
|
|
# I refer to these by their cardinal directions, like:
|
|
# [NW, N, NE,
|
|
# W, E,
|
|
# SW, S, SE]
|
|
DIRECTIONS = {
|
|
'NW': 0,
|
|
'N': 1,
|
|
'NE': 2,
|
|
'W': 3,
|
|
'E': 4,
|
|
'SW': 5,
|
|
'S': 6,
|
|
'SE': 7
|
|
}
|
|
def get(self, context, direction):
|
|
return context[self.DIRECTIONS[direction]]
|
|
|
|
class SquareModuleDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws the modules as simple squares
|
|
"""
|
|
fill = None
|
|
def initialize(self, styledPilImage, image):
|
|
self.imgDraw = ImageDraw.Draw(image)
|
|
self.fill = styledPilImage.paint_color
|
|
def drawrect_context(self, box, is_active, context):
|
|
if is_active:
|
|
self.imgDraw.rectangle(box, fill=self.fill)
|
|
|
|
class GappedSquareModuleDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws the modules as simple squares that are not contiguous.
|
|
The size_ratio determines how wide the squares are relative to the width of the space they are printed in
|
|
"""
|
|
fill = None
|
|
def __init__(self, size_ratio = 0.8):
|
|
self.size_ratio = size_ratio
|
|
def initialize(self, styledPilImage, image):
|
|
self.imgDraw = ImageDraw.Draw(image)
|
|
self.fill = styledPilImage.paint_color
|
|
self.delta = (1 - self.size_ratio) * styledPilImage.box_size / 2
|
|
def drawrect_context(self, box, is_active, context):
|
|
if is_active:
|
|
smaller_box = (
|
|
box[0][0] + self.delta,
|
|
box[0][1] + self.delta,
|
|
box[1][0] - self.delta,
|
|
box[1][1] - self.delta
|
|
)
|
|
self.imgDraw.rectangle(smaller_box, fill=self.fill)
|
|
|
|
class CircleModuleDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws the modules as circles
|
|
"""
|
|
circle = None
|
|
def initialize(self, styledPilImage, image):
|
|
box_size = styledPilImage.box_size
|
|
fake_size = box_size * ANTIALIASING_FACTOR
|
|
self.circle = Image.new(styledPilImage.mode,(fake_size, fake_size), styledPilImage.color_mask.back_color)
|
|
ImageDraw.Draw(self.circle).ellipse((0,0, fake_size, fake_size), fill = styledPilImage.paint_color)
|
|
self.circle = self.circle.resize((box_size, box_size), Image.LANCZOS)
|
|
self.image = image
|
|
def drawrect_context(self, box, is_active, context):
|
|
if is_active:
|
|
self.image.paste(self.circle, (box[0][0], box[0][1]))
|
|
|
|
class RoundedModuleDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws the modules with all 90 degree corners replaced with rounded edges
|
|
radius_ratio determines the radius of the rounded edges -
|
|
a value of 1 means that an isolated module will be drawn as a circle,
|
|
while a value of 0 means that the radius of the rounded edge will be 0 (and thus back to 90 degrees again)
|
|
"""
|
|
def __init__(self, radius_ratio = 1):
|
|
self.radius_ratio = radius_ratio
|
|
|
|
def initialize(self, styledPilImage, image):
|
|
self.corner_width = int(styledPilImage.box_size / 2)
|
|
self.image = image
|
|
self.setup_corners(styledPilImage.mode, styledPilImage.color_mask.back_color, styledPilImage.paint_color)
|
|
def setup_corners(self, mode, back_color, front_color):
|
|
self.SQUARE = Image.new(mode, (self.corner_width, self.corner_width), front_color)
|
|
|
|
fake_width = self.corner_width * ANTIALIASING_FACTOR
|
|
radius = self.radius_ratio * fake_width
|
|
diameter = radius * 2
|
|
base = Image.new(mode, (fake_width, fake_width), back_color) # make something 4x bigger for antialiasing
|
|
base_draw = ImageDraw.Draw(base)
|
|
base_draw.ellipse((0,0, diameter, diameter), fill = front_color)
|
|
base_draw.rectangle((radius, 0, fake_width, fake_width), fill = front_color)
|
|
base_draw.rectangle((0, radius, fake_width, fake_width), fill = front_color)
|
|
self.NW_ROUND = base.resize((self.corner_width, self.corner_width), Image.LANCZOS)
|
|
self.SW_ROUND = self.NW_ROUND.transpose(Image.FLIP_TOP_BOTTOM)
|
|
self.SE_ROUND = self.NW_ROUND.transpose(Image.ROTATE_180)
|
|
self.NE_ROUND = self.NW_ROUND.transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
def drawrect_context(self, box, is_active, context):
|
|
if not is_active:
|
|
return
|
|
# find rounded edges
|
|
nw_rounded = not self.get(context, 'W') and not self.get(context, 'N')
|
|
ne_rounded = not self.get(context, 'N') and not self.get(context, 'E')
|
|
se_rounded = not self.get(context, 'E') and not self.get(context, 'S')
|
|
sw_rounded = not self.get(context, 'S') and not self.get(context, 'W')
|
|
|
|
nw = self.NW_ROUND if nw_rounded else self.SQUARE
|
|
ne = self.NE_ROUND if ne_rounded else self.SQUARE
|
|
se = self.SE_ROUND if se_rounded else self.SQUARE
|
|
sw = self.SW_ROUND if sw_rounded else self.SQUARE
|
|
self.image.paste(nw, (box[0][0], box[0][1]))
|
|
self.image.paste(ne, (box[0][0] + self.corner_width, box[0][1]))
|
|
self.image.paste(se, (box[0][0] + self.corner_width, box[0][1] + self.corner_width))
|
|
self.image.paste(sw, (box[0][0], box[0][1] + self.corner_width))
|
|
|
|
|
|
class VerticalBarsDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws vertically contiguous groups of modules as long rounded rectangles, with gaps between neighboring bands
|
|
(the size of these gaps is inversely proportional to the horizontal_shrink)
|
|
"""
|
|
def __init__(self, horizontal_shrink = 0.8):
|
|
self.horizontal_shrink = horizontal_shrink
|
|
|
|
def initialize(self, styledPilImage, image):
|
|
self.half_height = int(styledPilImage.box_size / 2)
|
|
self.image = image
|
|
self.delta = int((1-self.horizontal_shrink) * self.half_height)
|
|
self.setup_edges(styledPilImage.mode, styledPilImage.color_mask.back_color, styledPilImage.paint_color)
|
|
|
|
def setup_edges(self, mode, back_color, front_color):
|
|
height = self.half_height
|
|
width = height * 2
|
|
shrunken_width = int(width * self.horizontal_shrink)
|
|
self.SQUARE = Image.new(mode, (shrunken_width, height), front_color)
|
|
|
|
fake_width = width * ANTIALIASING_FACTOR
|
|
fake_height = height * ANTIALIASING_FACTOR
|
|
radius = fake_width
|
|
base = Image.new(mode, (fake_width, fake_height), back_color) # make something 4x bigger for antialiasing
|
|
base_draw = ImageDraw.Draw(base)
|
|
base_draw.ellipse((0,0, fake_width, fake_height * 2), fill = front_color)
|
|
|
|
self.ROUND_TOP = base.resize((shrunken_width, height), Image.LANCZOS)
|
|
self.ROUND_BOTTOM = self.ROUND_TOP.transpose(Image.FLIP_TOP_BOTTOM)
|
|
|
|
def drawrect_context(self, box, is_active, context):
|
|
if is_active:
|
|
# find rounded edges
|
|
top_rounded = not self.get(context, 'N')
|
|
bottom_rounded = not self.get(context, 'S')
|
|
|
|
top = self.ROUND_TOP if top_rounded else self.SQUARE
|
|
bottom = self.ROUND_BOTTOM if bottom_rounded else self.SQUARE
|
|
self.image.paste(top, (box[0][0] + self.delta, box[0][1]))
|
|
self.image.paste(bottom, (box[0][0] + self.delta, box[0][1] + self.half_height))
|
|
|
|
class HorizontalBarsDrawer(QRModuleDrawer):
|
|
"""
|
|
Draws horizontally contiguous groups of modules as long rounded rectangles, with gaps between neighboring bands
|
|
(the size of these gaps is inversely proportional to the vertical_shrink)
|
|
"""
|
|
def __init__(self, vertical_shrink = 0.8):
|
|
self.vertical_shrink = vertical_shrink
|
|
|
|
def initialize(self, styledPilImage, image):
|
|
self.half_width = int(styledPilImage.box_size / 2)
|
|
self.image = image
|
|
self.delta = int((1-self.vertical_shrink) * self.half_width)
|
|
self.setup_edges(styledPilImage.mode, styledPilImage.color_mask.back_color, styledPilImage.paint_color)
|
|
|
|
def setup_edges(self, mode, back_color, front_color):
|
|
width = self.half_width
|
|
height = width * 2
|
|
shrunken_height= int(height * self.vertical_shrink)
|
|
self.SQUARE = Image.new(mode, (width, shrunken_height), front_color)
|
|
|
|
fake_width = width * ANTIALIASING_FACTOR
|
|
fake_height = height * ANTIALIASING_FACTOR
|
|
radius = fake_height
|
|
base = Image.new(mode, (fake_width, fake_height), back_color) # make something 4x bigger for antialiasing
|
|
base_draw = ImageDraw.Draw(base)
|
|
base_draw.ellipse((0,0, fake_width * 2, fake_height), fill = front_color)
|
|
|
|
self.ROUND_LEFT = base.resize((width, shrunken_height), Image.LANCZOS)
|
|
self.ROUND_RIGHT = self.ROUND_LEFT.transpose(Image.FLIP_LEFT_RIGHT)
|
|
|
|
def drawrect_context(self, box, is_active, context):
|
|
if is_active:
|
|
# find rounded edges
|
|
left_rounded = not self.get(context, 'W')
|
|
right_rounded = not self.get(context, 'E')
|
|
|
|
left = self.ROUND_LEFT if left_rounded else self.SQUARE
|
|
right = self.ROUND_RIGHT if right_rounded else self.SQUARE
|
|
self.image.paste(left, (box[0][0], box[0][1] + self.delta))
|
|
self.image.paste(right, (box[0][0] + self.half_width, box[0][1] + self.delta))
|