Skip to content

Commit

Permalink
Extend drawers to work with svgs
Browse files Browse the repository at this point in the history
  • Loading branch information
SmileyChris committed Feb 3, 2022
1 parent 628eaf9 commit 2287382
Show file tree
Hide file tree
Showing 8 changed files with 425 additions and 219 deletions.
71 changes: 69 additions & 2 deletions qrcode/image/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import abc
from typing import TYPE_CHECKING, Any, Optional, Tuple, Type, Union

if TYPE_CHECKING:
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
from qrcode.main import ActiveWithNeighbors, QRCode


class BaseImage:
"""
Expand All @@ -18,14 +22,15 @@ def __init__(self, border, width, box_size, *args, **kwargs):
self.box_size = box_size
self.pixel_size = (self.width + self.border * 2) * self.box_size
self._img = self.new_image(**kwargs)
self.init_new_image()

@abc.abstractmethod
def drawrect(self, row, col):
"""
Draw a single rectangle of the QR code.
"""

def drawrect_context(self, row, col, active, context):
def drawrect_context(self, row: int, col: int, qr: "QRCode"):
"""
Draw a single rectangle of the QR code given the surrounding context
"""
Expand All @@ -50,14 +55,20 @@ def pixel_box(self, row, col):
"""
x = (col + self.border) * self.box_size
y = (row + self.border) * self.box_size
return [(x, y), (x + self.box_size - 1, y + self.box_size - 1)]
return (
(x, y),
(x + self.box_size - 1, y + self.box_size - 1),
)

@abc.abstractmethod
def new_image(self, **kwargs) -> Any:
"""
Build the image class. Subclasses should return the class created.
"""

def init_new_image(self):
pass

def get_image(self, **kwargs):
"""
Return the image class for further processing.
Expand All @@ -78,3 +89,59 @@ def check_kind(self, kind, transform=None):
if not allowed:
raise ValueError(f"Cannot set {type(self).__name__} type to {kind}")
return kind

def is_eye(self, row: int, col: int):
"""
Find whether the referenced module is in an eye.
"""
return (
(row < 7 and col < 7)
or (row < 7 and self.width - col < 8)
or (self.width - row < 8 and col < 7)
)


class BaseImageWithDrawer(BaseImage):
default_drawer_class: "Type[QRModuleDrawer]"

def get_default_module_drawer(self) -> "QRModuleDrawer":
return self.default_drawer_class()

def get_default_eye_drawer(self) -> "QRModuleDrawer":
return self.default_drawer_class()

needs_context = True

module_drawer: "QRModuleDrawer"
eye_drawer: "QRModuleDrawer"

def __init__(
self,
*args,
module_drawer: "QRModuleDrawer" = None,
eye_drawer: "QRModuleDrawer" = None,
**kwargs,
):
self.module_drawer = module_drawer or self.get_default_module_drawer()
# The eye drawer can be overridden by another module drawer as well,
# but you have to be more careful with these in order to make the QR
# code still parseable
self.eye_drawer = eye_drawer or self.get_default_eye_drawer()
super().__init__(*args, **kwargs)

def init_new_image(self):
self.module_drawer.initialize(img=self)
self.eye_drawer.initialize(img=self)

return super().init_new_image()

def drawrect_context(self, row: int, col: int, qr: "QRCode"):
box = self.pixel_box(row, col)
drawer = self.eye_drawer if self.is_eye(row, col) else self.module_drawer
is_active: Union[bool, ActiveWithNeighbors] = (
qr.active_with_neighbors(row, col)
if drawer.needs_neighbors
else bool(qr.modules[row][col])
)

drawer.drawrect(box, is_active)
65 changes: 22 additions & 43 deletions qrcode/image/styledpil.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import Image

import qrcode.image.base
from qrcode.image.styles.colormasks import SolidFillColorMask
from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask
from qrcode.image.styles.moduledrawers import SquareModuleDrawer


class StyledPilImage(qrcode.image.base.BaseImage):
class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
"""
Styled PIL image builder, default format is PNG.
Expand Down Expand Up @@ -41,24 +41,30 @@ class StyledPilImage(qrcode.image.base.BaseImage):

kind = "PNG"

needs_context = True
needs_processing = True
color_mask: QRColorMask
default_drawer_class = SquareModuleDrawer

def new_image(self, **kwargs):
def __init__(self, *args, **kwargs):
self.color_mask = kwargs.get("color_mask", SolidFillColorMask())
self.module_drawer = kwargs.get("module_drawer", SquareModuleDrawer())
# The eye drawer can be overridden by another module drawer as well,
# but you have to be more careful with these in order to make the QR
# code still parseable
self.eye_drawer = kwargs.get("eye_drawer", SquareModuleDrawer())

embeded_image_path = kwargs.get("embeded_image_path", None)
self.embeded_image = kwargs.get("embeded_image", None)
self.embeded_image_resample = kwargs.get(
"embeded_image_resample", Image.LANCZOS
)
if not self.embeded_image and embeded_image_path:
self.embeded_image = Image.open(embeded_image_path)

# the paint_color is the color the module drawer will use to draw upon
# a canvas During the color mask process, pixels that are paint_color
# are replaced by a newly-calculated color
self.paint_color = tuple(0 for i in self.color_mask.back_color)
if self.color_mask.has_transparency:
self.paint_color = tuple([*self.color_mask.back_color[:3], 255])

super().__init__(*args, **kwargs)

def new_image(self, **kwargs):
mode = (
"RGBA"
if (
Expand All @@ -67,32 +73,14 @@ def new_image(self, **kwargs):
)
else "RGB"
)
self.mode = mode
# This is the background color. Should be white or whiteish
back_color = self.color_mask.back_color

self.back_color = (
self.color_mask.back_color
) # This is the background color. Should be white or whiteish
return Image.new(mode, (self.pixel_size, self.pixel_size), back_color)

img = Image.new(mode, (self.pixel_size, self.pixel_size), self.back_color)

# the paint_color is the color the module drawer will use to draw upon
# a canvas During the color mask process, pixels that are paint_color
# are replaced by a newly-calculated color
self.paint_color = tuple(0 for i in self.color_mask.back_color)
if self.color_mask.has_transparency:
self.paint_color = tuple([*self.color_mask.back_color[:3], 255])

self.color_mask.initialize(self, img)
self.module_drawer.initialize(self, img)
self.eye_drawer.initialize(self, img)
return img

def drawrect_context(self, row, col, is_active, context):
box = self.pixel_box(row, col)
if self.is_eye(row, col):
self.eye_drawer.drawrect_context(box, is_active, context)
else:
self.module_drawer.drawrect_context(box, is_active, context)
def init_new_image(self):
self.color_mask.initialize(self, self._img)
super().init_new_image()

def process(self):
self.color_mask.apply_mask(self._img)
Expand All @@ -116,15 +104,6 @@ def draw_embeded_image(self):
else:
self._img.paste(region, logo_position)

# The eyes are treated differently, and this will find whether the
# referenced module is in an eye
def is_eye(self, row, col):
return (
(row < 7 and col < 7)
or (row < 7 and self.width - col < 8)
or (self.width - row < 8 and col < 7)
)

def save(self, stream, format=None, **kwargs):
if format is None:
format = kwargs.get("kind", self.kind)
Expand Down
7 changes: 7 additions & 0 deletions qrcode/image/styles/moduledrawers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# For backwards compatibility, importing the PIL drawers here.
from .pil import CircleModuleDrawer # noqa: F401
from .pil import GappedSquareModuleDrawer # noqa: F401
from .pil import HorizontalBarsDrawer # noqa: F401
from .pil import RoundedModuleDrawer # noqa: F401
from .pil import SquareModuleDrawer # noqa: F401
from .pil import VerticalBarsDrawer # noqa: F401
34 changes: 34 additions & 0 deletions qrcode/image/styles/moduledrawers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import absolute_import

import abc
from typing import TYPE_CHECKING, Union

if TYPE_CHECKING:
from qrcode.image.base import BaseImage
from qrcode.main import ActiveWithNeighbors


class QRModuleDrawer(abc.ABC):
"""
QRModuleDrawer exists to draw the modules of the QR Code onto images.
For this, technically all that is necessary is a ``drawrect(self, box,
is_active)`` function which takes in the box in which it is to draw,
whether or not the box is "active" (a module exists there). If
``needs_neighbors`` is set to True, then the method should also accept a
``neighbors`` kwarg (the neighboring pixels).
It is frequently necessary to also implement an "initialize" function to
set up values that only the containing Image class knows about.
For examples of what these look like, see doc/module_drawers.png
"""

needs_neighbors = False

def initialize(self, img: "BaseImage") -> None:
self.img = img

@abc.abstractmethod
def drawrect(self, box, is_active: "Union[bool, ActiveWithNeighbors]") -> None:
...
Loading

0 comments on commit 2287382

Please sign in to comment.