This commit is contained in:
Iliyan Angelov
2025-09-19 11:58:53 +03:00
parent 306b20e24a
commit 6b247e5b9f
11423 changed files with 1500615 additions and 778 deletions

View File

@@ -0,0 +1,164 @@
import abc
from typing import TYPE_CHECKING, Any, Optional, Union
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
if TYPE_CHECKING:
from qrcode.main import ActiveWithNeighbors, QRCode
DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]]
class BaseImage:
"""
Base QRCode image output class.
"""
kind: Optional[str] = None
allowed_kinds: Optional[tuple[str]] = None
needs_context = False
needs_processing = False
needs_drawrect = True
def __init__(self, border, width, box_size, *args, **kwargs):
self.border = border
self.width = width
self.box_size = box_size
self.pixel_size = (self.width + self.border * 2) * self.box_size
self.modules = kwargs.pop("qrcode_modules")
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: int, col: int, qr: "QRCode"):
"""
Draw a single rectangle of the QR code given the surrounding context
"""
raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover
def process(self):
"""
Processes QR code after completion
"""
raise NotImplementedError("BaseImage.drawimage") # pragma: no cover
@abc.abstractmethod
def save(self, stream, kind=None):
"""
Save the image file.
"""
def pixel_box(self, row, col):
"""
A helper method for pixel-based image generators that specifies the
four pixel coordinates for a single rect.
"""
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),
)
@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.
"""
return self._img
def check_kind(self, kind, transform=None):
"""
Get the image type.
"""
if kind is None:
kind = self.kind
allowed = not self.allowed_kinds or kind in self.allowed_kinds
if transform:
kind = transform(kind)
if not allowed:
allowed = kind in self.allowed_kinds
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]
drawer_aliases: DrawerAliases = {}
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: Union[QRModuleDrawer, str, None] = None,
eye_drawer: Union[QRModuleDrawer, str, None] = None,
**kwargs,
):
self.module_drawer = (
self.get_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 = self.get_drawer(eye_drawer) or self.get_default_eye_drawer()
super().__init__(*args, **kwargs)
def get_drawer(
self, drawer: Union[QRModuleDrawer, str, None]
) -> Optional[QRModuleDrawer]:
if not isinstance(drawer, str):
return drawer
drawer_cls, kwargs = self.drawer_aliases[drawer]
return drawer_cls(**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)

View File

@@ -0,0 +1,57 @@
import qrcode.image.base
from PIL import Image, ImageDraw
class PilImage(qrcode.image.base.BaseImage):
"""
PIL image builder, default format is PNG.
"""
kind = "PNG"
def new_image(self, **kwargs):
if not Image:
raise ImportError("PIL library not found.")
back_color = kwargs.get("back_color", "white")
fill_color = kwargs.get("fill_color", "black")
try:
fill_color = fill_color.lower()
except AttributeError:
pass
try:
back_color = back_color.lower()
except AttributeError:
pass
# L mode (1 mode) color = (r*299 + g*587 + b*114)//1000
if fill_color == "black" and back_color == "white":
mode = "1"
fill_color = 0
if back_color == "white":
back_color = 255
elif back_color == "transparent":
mode = "RGBA"
back_color = None
else:
mode = "RGB"
img = Image.new(mode, (self.pixel_size, self.pixel_size), back_color)
self.fill_color = fill_color
self._idr = ImageDraw.Draw(img)
return img
def drawrect(self, row, col):
box = self.pixel_box(row, col)
self._idr.rectangle(box, fill=self.fill_color)
def save(self, stream, format=None, **kwargs):
kind = kwargs.pop("kind", self.kind)
if format is None:
format = kind
self._img.save(stream, format=format, **kwargs)
def __getattr__(self, name):
return getattr(self._img, name)

View File

@@ -0,0 +1,56 @@
from itertools import chain
from qrcode.compat.png import PngWriter
from qrcode.image.base import BaseImage
class PyPNGImage(BaseImage):
"""
pyPNG image builder.
"""
kind = "PNG"
allowed_kinds = ("PNG",)
needs_drawrect = False
def new_image(self, **kwargs):
if not PngWriter:
raise ImportError("PyPNG library not installed.")
return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1)
def drawrect(self, row, col):
"""
Not used.
"""
def save(self, stream, kind=None):
if isinstance(stream, str):
stream = open(stream, "wb")
self._img.write(stream, self.rows_iter())
def rows_iter(self):
yield from self.border_rows_iter()
border_col = [1] * (self.box_size * self.border)
for module_row in self.modules:
row = (
border_col
+ list(
chain.from_iterable(
([not point] * self.box_size) for point in module_row
)
)
+ border_col
)
for _ in range(self.box_size):
yield row
yield from self.border_rows_iter()
def border_rows_iter(self):
border_row = [1] * (self.box_size * (self.width + self.border * 2))
for _ in range(self.border * self.box_size):
yield border_row
# Keeping this for backwards compatibility.
PymagingImage = PyPNGImage

View File

@@ -0,0 +1,120 @@
import qrcode.image.base
from PIL import Image
from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask
from qrcode.image.styles.moduledrawers import SquareModuleDrawer
class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
"""
Styled PIL image builder, default format is PNG.
This differs from the PilImage in that there is a module_drawer, a
color_mask, and an optional image
The module_drawer should extend the QRModuleDrawer class and implement the
drawrect_context(self, box, active, context), and probably also the
initialize function. This will draw an individual "module" or square on
the QR code.
The color_mask will extend the QRColorMask class and will at very least
implement the get_fg_pixel(image, x, y) function, calculating a color to
put on the image at the pixel location (x,y) (more advanced functionality
can be gotten by instead overriding other functions defined in the
QRColorMask class)
The Image can be specified either by path or with a Pillow Image, and if it
is there will be placed in the middle of the QR code. No effort is done to
ensure that the QR code is still legible after the image has been placed
there; Q or H level error correction levels are recommended to maintain
data integrity A resampling filter can be specified (defaulting to
PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible
options for this parameter.
The image size can be controlled by `embedded_image_ratio` which is a ratio
between 0 and 1 that's set in relation to the overall width of the QR code.
"""
kind = "PNG"
needs_processing = True
color_mask: QRColorMask
default_drawer_class = SquareModuleDrawer
def __init__(self, *args, **kwargs):
self.color_mask = kwargs.get("color_mask", SolidFillColorMask())
# allow embeded_ parameters with typos for backwards compatibility
embedded_image_path = kwargs.get(
"embedded_image_path", kwargs.get("embeded_image_path", None)
)
self.embedded_image = kwargs.get(
"embedded_image", kwargs.get("embeded_image", None)
)
self.embedded_image_ratio = kwargs.get(
"embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25)
)
self.embedded_image_resample = kwargs.get(
"embedded_image_resample",
kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS),
)
if not self.embedded_image and embedded_image_path:
self.embedded_image = Image.open(embedded_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 (
self.color_mask.has_transparency
or (self.embedded_image and "A" in self.embedded_image.getbands())
)
else "RGB"
)
# This is the background color. Should be white or whiteish
back_color = self.color_mask.back_color
return Image.new(mode, (self.pixel_size, self.pixel_size), back_color)
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)
if self.embedded_image:
self.draw_embedded_image()
def draw_embedded_image(self):
if not self.embedded_image:
return
total_width, _ = self._img.size
total_width = int(total_width)
logo_width_ish = int(total_width * self.embedded_image_ratio)
logo_offset = (
int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size)
* self.box_size
) # round the offset to the nearest module
logo_position = (logo_offset, logo_offset)
logo_width = total_width - logo_offset * 2
region = self.embedded_image
region = region.resize((logo_width, logo_width), self.embedded_image_resample)
if "A" in region.getbands():
self._img.alpha_composite(region, logo_position)
else:
self._img.paste(region, logo_position)
def save(self, stream, format=None, **kwargs):
if format is None:
format = kwargs.get("kind", self.kind)
if "kind" in kwargs:
del kwargs["kind"]
self._img.save(stream, format=format, **kwargs)
def __getattr__(self, name):
return getattr(self._img, name)

View File

@@ -0,0 +1,226 @@
import math
from PIL import Image
class QRColorMask:
"""
QRColorMask is used to color in the QRCode.
By the time apply_mask is called, the QRModuleDrawer of the StyledPilImage
will have drawn all of the modules on the canvas (the color of these
modules will be mostly black, although antialiasing may result in
gradients) In the base class, apply_mask is implemented such that the
background color will remain, but the foreground pixels will be replaced by
a color determined by a call to get_fg_pixel. There is additional
calculation done to preserve the gradient artifacts of antialiasing.
All QRColorMask objects should be careful about RGB vs RGBA color spaces.
For examples of what these look like, see doc/color_masks.png
"""
back_color = (255, 255, 255)
has_transparency = False
paint_color = back_color
def initialize(self, styledPilImage, image):
self.paint_color = styledPilImage.paint_color
def apply_mask(self, image, use_cache=False):
width, height = image.size
pixels = image.load()
fg_color_cache = {} if use_cache else None
for x in range(width):
for y in range(height):
current_color = pixels[x, y]
if current_color == self.back_color:
continue
if use_cache and current_color in fg_color_cache:
pixels[x, y] = fg_color_cache[current_color]
continue
norm = self.extrap_color(
self.back_color, self.paint_color, current_color
)
if norm is not None:
new_color = self.interp_color(
self.get_bg_pixel(image, x, y),
self.get_fg_pixel(image, x, y),
norm,
)
pixels[x, y] = new_color
if use_cache:
fg_color_cache[current_color] = new_color
else:
pixels[x, y] = self.get_bg_pixel(image, x, y)
def get_fg_pixel(self, image, x, y):
raise NotImplementedError("QRModuleDrawer.paint_fg_pixel")
def get_bg_pixel(self, image, x, y):
return self.back_color
# The following functions are helpful for color calculation:
# interpolate a number between two numbers
def interp_num(self, n1, n2, norm):
return int(n2 * norm + n1 * (1 - norm))
# interpolate a color between two colorrs
def interp_color(self, col1, col2, norm):
return tuple(self.interp_num(col1[i], col2[i], norm) for i in range(len(col1)))
# find the interpolation coefficient between two numbers
def extrap_num(self, n1, n2, interped_num):
if n2 == n1:
return None
else:
return (interped_num - n1) / (n2 - n1)
# find the interpolation coefficient between two numbers
def extrap_color(self, col1, col2, interped_color):
normed = []
for c1, c2, ci in zip(col1, col2, interped_color):
extrap = self.extrap_num(c1, c2, ci)
if extrap is not None:
normed.append(extrap)
if not normed:
return None
return sum(normed) / len(normed)
class SolidFillColorMask(QRColorMask):
"""
Just fills in the background with one color and the foreground with another
"""
def __init__(self, back_color=(255, 255, 255), front_color=(0, 0, 0)):
self.back_color = back_color
self.front_color = front_color
self.has_transparency = len(self.back_color) == 4
def apply_mask(self, image):
if self.back_color == (255, 255, 255) and self.front_color == (0, 0, 0):
# Optimization: the image is already drawn by QRModuleDrawer in
# black and white, so if these are also our mask colors we don't
# need to do anything. This is much faster than actually applying a
# mask.
pass
else:
# TODO there's probably a way to use PIL.ImageMath instead of doing
# the individual pixel comparisons that the base class uses, which
# would be a lot faster. (In fact doing this would probably remove
# the need for the B&W optimization above.)
QRColorMask.apply_mask(self, image, use_cache=True)
def get_fg_pixel(self, image, x, y):
return self.front_color
class RadialGradiantColorMask(QRColorMask):
"""
Fills in the foreground with a radial gradient from the center to the edge
"""
def __init__(
self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255)
):
self.back_color = back_color
self.center_color = center_color
self.edge_color = edge_color
self.has_transparency = len(self.back_color) == 4
def get_fg_pixel(self, image, x, y):
width, _ = image.size
normedDistanceToCenter = math.sqrt(
(x - width / 2) ** 2 + (y - width / 2) ** 2
) / (math.sqrt(2) * width / 2)
return self.interp_color(
self.center_color, self.edge_color, normedDistanceToCenter
)
class SquareGradiantColorMask(QRColorMask):
"""
Fills in the foreground with a square gradient from the center to the edge
"""
def __init__(
self, back_color=(255, 255, 255), center_color=(0, 0, 0), edge_color=(0, 0, 255)
):
self.back_color = back_color
self.center_color = center_color
self.edge_color = edge_color
self.has_transparency = len(self.back_color) == 4
def get_fg_pixel(self, image, x, y):
width, _ = image.size
normedDistanceToCenter = max(abs(x - width / 2), abs(y - width / 2)) / (
width / 2
)
return self.interp_color(
self.center_color, self.edge_color, normedDistanceToCenter
)
class HorizontalGradiantColorMask(QRColorMask):
"""
Fills in the foreground with a gradient sweeping from the left to the right
"""
def __init__(
self, back_color=(255, 255, 255), left_color=(0, 0, 0), right_color=(0, 0, 255)
):
self.back_color = back_color
self.left_color = left_color
self.right_color = right_color
self.has_transparency = len(self.back_color) == 4
def get_fg_pixel(self, image, x, y):
width, _ = image.size
return self.interp_color(self.left_color, self.right_color, x / width)
class VerticalGradiantColorMask(QRColorMask):
"""
Fills in the forefround with a gradient sweeping from the top to the bottom
"""
def __init__(
self, back_color=(255, 255, 255), top_color=(0, 0, 0), bottom_color=(0, 0, 255)
):
self.back_color = back_color
self.top_color = top_color
self.bottom_color = bottom_color
self.has_transparency = len(self.back_color) == 4
def get_fg_pixel(self, image, x, y):
width, _ = image.size
return self.interp_color(self.top_color, self.bottom_color, y / width)
class ImageColorMask(QRColorMask):
"""
Fills in the foreground with pixels from another image, either passed by
path or passed by image object.
"""
def __init__(
self, back_color=(255, 255, 255), color_mask_path=None, color_mask_image=None
):
self.back_color = back_color
if color_mask_image:
self.color_img = color_mask_image
else:
self.color_img = Image.open(color_mask_path)
self.has_transparency = len(self.back_color) == 4
def initialize(self, styledPilImage, image):
self.paint_color = styledPilImage.paint_color
self.color_img = self.color_img.resize(image.size)
def get_fg_pixel(self, image, x, y):
width, _ = image.size
return self.color_img.getpixel((x, y))

View File

@@ -0,0 +1,10 @@
# For backwards compatibility, importing the PIL drawers here.
try:
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
except ImportError:
pass

View File

@@ -0,0 +1,33 @@
import abc
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from qrcode.image.base import BaseImage
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 __init__(self, **kwargs):
pass
def initialize(self, img: "BaseImage") -> None:
self.img = img
@abc.abstractmethod
def drawrect(self, box, is_active) -> None: ...

View File

@@ -0,0 +1,265 @@
from typing import TYPE_CHECKING
from PIL import Image, ImageDraw
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
if TYPE_CHECKING:
from qrcode.image.styledpil import StyledPilImage
from qrcode.main import ActiveWithNeighbors
# When drawing antialiased things, make them bigger and then shrink them down
# to size after the geometry has been drawn.
ANTIALIASING_FACTOR = 4
class StyledPilQRModuleDrawer(QRModuleDrawer):
"""
A base class for StyledPilImage module drawers.
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
"""
img: "StyledPilImage"
class SquareModuleDrawer(StyledPilQRModuleDrawer):
"""
Draws the modules as simple squares
"""
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
self.imgDraw = ImageDraw.Draw(self.img._img)
def drawrect(self, box, is_active: bool):
if is_active:
self.imgDraw.rectangle(box, fill=self.img.paint_color)
class GappedSquareModuleDrawer(StyledPilQRModuleDrawer):
"""
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
"""
def __init__(self, size_ratio=0.8):
self.size_ratio = size_ratio
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
self.imgDraw = ImageDraw.Draw(self.img._img)
self.delta = (1 - self.size_ratio) * self.img.box_size / 2
def drawrect(self, box, is_active: bool):
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.img.paint_color)
class CircleModuleDrawer(StyledPilQRModuleDrawer):
"""
Draws the modules as circles
"""
circle = None
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
box_size = self.img.box_size
fake_size = box_size * ANTIALIASING_FACTOR
self.circle = Image.new(
self.img.mode,
(fake_size, fake_size),
self.img.color_mask.back_color,
)
ImageDraw.Draw(self.circle).ellipse(
(0, 0, fake_size, fake_size), fill=self.img.paint_color
)
self.circle = self.circle.resize((box_size, box_size), Image.Resampling.LANCZOS)
def drawrect(self, box, is_active: bool):
if is_active:
self.img._img.paste(self.circle, (box[0][0], box[0][1]))
class RoundedModuleDrawer(StyledPilQRModuleDrawer):
"""
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).
"""
needs_neighbors = True
def __init__(self, radius_ratio=1):
self.radius_ratio = radius_ratio
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
self.corner_width = int(self.img.box_size / 2)
self.setup_corners()
def setup_corners(self):
mode = self.img.mode
back_color = self.img.color_mask.back_color
front_color = self.img.paint_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.Resampling.LANCZOS
)
self.SW_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
self.SE_ROUND = self.NW_ROUND.transpose(Image.Transpose.ROTATE_180)
self.NE_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
def drawrect(self, box: list[list[int]], is_active: "ActiveWithNeighbors"):
if not is_active:
return
# find rounded edges
nw_rounded = not is_active.W and not is_active.N
ne_rounded = not is_active.N and not is_active.E
se_rounded = not is_active.E and not is_active.S
sw_rounded = not is_active.S and not is_active.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.img._img.paste(nw, (box[0][0], box[0][1]))
self.img._img.paste(ne, (box[0][0] + self.corner_width, box[0][1]))
self.img._img.paste(
se, (box[0][0] + self.corner_width, box[0][1] + self.corner_width)
)
self.img._img.paste(sw, (box[0][0], box[0][1] + self.corner_width))
class VerticalBarsDrawer(StyledPilQRModuleDrawer):
"""
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).
"""
needs_neighbors = True
def __init__(self, horizontal_shrink=0.8):
self.horizontal_shrink = horizontal_shrink
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
self.half_height = int(self.img.box_size / 2)
self.delta = int((1 - self.horizontal_shrink) * self.half_height)
self.setup_edges()
def setup_edges(self):
mode = self.img.mode
back_color = self.img.color_mask.back_color
front_color = self.img.paint_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
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.Resampling.LANCZOS)
self.ROUND_BOTTOM = self.ROUND_TOP.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
def drawrect(self, box, is_active: "ActiveWithNeighbors"):
if is_active:
# find rounded edges
top_rounded = not is_active.N
bottom_rounded = not is_active.S
top = self.ROUND_TOP if top_rounded else self.SQUARE
bottom = self.ROUND_BOTTOM if bottom_rounded else self.SQUARE
self.img._img.paste(top, (box[0][0] + self.delta, box[0][1]))
self.img._img.paste(
bottom, (box[0][0] + self.delta, box[0][1] + self.half_height)
)
class HorizontalBarsDrawer(StyledPilQRModuleDrawer):
"""
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).
"""
needs_neighbors = True
def __init__(self, vertical_shrink=0.8):
self.vertical_shrink = vertical_shrink
def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)
self.half_width = int(self.img.box_size / 2)
self.delta = int((1 - self.vertical_shrink) * self.half_width)
self.setup_edges()
def setup_edges(self):
mode = self.img.mode
back_color = self.img.color_mask.back_color
front_color = self.img.paint_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
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.Resampling.LANCZOS
)
self.ROUND_RIGHT = self.ROUND_LEFT.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
def drawrect(self, box, is_active: "ActiveWithNeighbors"):
if is_active:
# find rounded edges
left_rounded = not is_active.W
right_rounded = not is_active.E
left = self.ROUND_LEFT if left_rounded else self.SQUARE
right = self.ROUND_RIGHT if right_rounded else self.SQUARE
self.img._img.paste(left, (box[0][0], box[0][1] + self.delta))
self.img._img.paste(
right, (box[0][0] + self.half_width, box[0][1] + self.delta)
)

View File

@@ -0,0 +1,139 @@
import abc
from decimal import Decimal
from typing import TYPE_CHECKING, NamedTuple
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
from qrcode.compat.etree import ET
if TYPE_CHECKING:
from qrcode.image.svg import SvgFragmentImage, SvgPathImage
ANTIALIASING_FACTOR = 4
class Coords(NamedTuple):
x0: Decimal
y0: Decimal
x1: Decimal
y1: Decimal
xh: Decimal
yh: Decimal
class BaseSvgQRModuleDrawer(QRModuleDrawer):
img: "SvgFragmentImage"
def __init__(self, *, size_ratio: Decimal = Decimal(1), **kwargs):
self.size_ratio = size_ratio
def initialize(self, *args, **kwargs) -> None:
super().initialize(*args, **kwargs)
self.box_delta = (1 - self.size_ratio) * self.img.box_size / 2
self.box_size = Decimal(self.img.box_size) * self.size_ratio
self.box_half = self.box_size / 2
def coords(self, box) -> Coords:
row, col = box[0]
x = row + self.box_delta
y = col + self.box_delta
return Coords(
x,
y,
x + self.box_size,
y + self.box_size,
x + self.box_half,
y + self.box_half,
)
class SvgQRModuleDrawer(BaseSvgQRModuleDrawer):
tag = "rect"
def initialize(self, *args, **kwargs) -> None:
super().initialize(*args, **kwargs)
self.tag_qname = ET.QName(self.img._SVG_namespace, self.tag)
def drawrect(self, box, is_active: bool):
if not is_active:
return
self.img._img.append(self.el(box))
@abc.abstractmethod
def el(self, box): ...
class SvgSquareDrawer(SvgQRModuleDrawer):
def initialize(self, *args, **kwargs) -> None:
super().initialize(*args, **kwargs)
self.unit_size = self.img.units(self.box_size)
def el(self, box):
coords = self.coords(box)
return ET.Element(
self.tag_qname, # type: ignore
x=self.img.units(coords.x0),
y=self.img.units(coords.y0),
width=self.unit_size,
height=self.unit_size,
)
class SvgCircleDrawer(SvgQRModuleDrawer):
tag = "circle"
def initialize(self, *args, **kwargs) -> None:
super().initialize(*args, **kwargs)
self.radius = self.img.units(self.box_half)
def el(self, box):
coords = self.coords(box)
return ET.Element(
self.tag_qname, # type: ignore
cx=self.img.units(coords.xh),
cy=self.img.units(coords.yh),
r=self.radius,
)
class SvgPathQRModuleDrawer(BaseSvgQRModuleDrawer):
img: "SvgPathImage"
def drawrect(self, box, is_active: bool):
if not is_active:
return
self.img._subpaths.append(self.subpath(box))
@abc.abstractmethod
def subpath(self, box) -> str: ...
class SvgPathSquareDrawer(SvgPathQRModuleDrawer):
def subpath(self, box) -> str:
coords = self.coords(box)
x0 = self.img.units(coords.x0, text=False)
y0 = self.img.units(coords.y0, text=False)
x1 = self.img.units(coords.x1, text=False)
y1 = self.img.units(coords.y1, text=False)
return f"M{x0},{y0}H{x1}V{y1}H{x0}z"
class SvgPathCircleDrawer(SvgPathQRModuleDrawer):
def initialize(self, *args, **kwargs) -> None:
super().initialize(*args, **kwargs)
def subpath(self, box) -> str:
coords = self.coords(box)
x0 = self.img.units(coords.x0, text=False)
yh = self.img.units(coords.yh, text=False)
h = self.img.units(self.box_half - self.box_delta, text=False)
x1 = self.img.units(coords.x1, text=False)
# rx,ry is the centerpoint of the arc
# 1? is the x-axis-rotation
# 2? is the large-arc-flag
# 3? is the sweep flag
# x,y is the point the arc is drawn to
return f"M{x0},{yh}A{h},{h} 0 0 0 {x1},{yh}A{h},{h} 0 0 0 {x0},{yh}z"

View File

@@ -0,0 +1,175 @@
import decimal
from decimal import Decimal
from typing import Optional, Union, overload, Literal
import qrcode.image.base
from qrcode.compat.etree import ET
from qrcode.image.styles.moduledrawers import svg as svg_drawers
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer):
"""
SVG image builder
Creates a QR-code image as a SVG document fragment.
"""
_SVG_namespace = "http://www.w3.org/2000/svg"
kind = "SVG"
allowed_kinds = ("SVG",)
default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer
def __init__(self, *args, **kwargs):
ET.register_namespace("svg", self._SVG_namespace)
super().__init__(*args, **kwargs)
# Save the unit size, for example the default box_size of 10 is '1mm'.
self.unit_size = self.units(self.box_size)
@overload
def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ...
@overload
def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ...
def units(self, pixels, text=True):
"""
A box_size of 10 (default) equals 1mm.
"""
units = Decimal(pixels) / 10
if not text:
return units
units = units.quantize(Decimal("0.001"))
context = decimal.Context(traps=[decimal.Inexact])
try:
for d in (Decimal("0.01"), Decimal("0.1"), Decimal("0")):
units = units.quantize(d, context=context)
except decimal.Inexact:
pass
return f"{units}mm"
def save(self, stream, kind=None):
self.check_kind(kind=kind)
self._write(stream)
def to_string(self, **kwargs):
return ET.tostring(self._img, **kwargs)
def new_image(self, **kwargs):
return self._svg(**kwargs)
def _svg(self, tag=None, version="1.1", **kwargs):
if tag is None:
tag = ET.QName(self._SVG_namespace, "svg")
dimension = self.units(self.pixel_size)
return ET.Element(
tag, # type: ignore
width=dimension,
height=dimension,
version=version,
**kwargs,
)
def _write(self, stream):
ET.ElementTree(self._img).write(stream, xml_declaration=False)
class SvgImage(SvgFragmentImage):
"""
Standalone SVG image builder
Creates a QR-code image as a standalone SVG document.
"""
background: Optional[str] = None
drawer_aliases: qrcode.image.base.DrawerAliases = {
"circle": (svg_drawers.SvgCircleDrawer, {}),
"gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}),
"gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}),
}
def _svg(self, tag="svg", **kwargs):
svg = super()._svg(tag=tag, **kwargs)
svg.set("xmlns", self._SVG_namespace)
if self.background:
svg.append(
ET.Element(
"rect",
fill=self.background,
x="0",
y="0",
width="100%",
height="100%",
)
)
return svg
def _write(self, stream):
ET.ElementTree(self._img).write(stream, encoding="UTF-8", xml_declaration=True)
class SvgPathImage(SvgImage):
"""
SVG image builder with one single <path> element (removes white spaces
between individual QR points).
"""
QR_PATH_STYLE = {
"fill": "#000000",
"fill-opacity": "1",
"fill-rule": "nonzero",
"stroke": "none",
}
needs_processing = True
path: Optional[ET.Element] = None
default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer
drawer_aliases = {
"circle": (svg_drawers.SvgPathCircleDrawer, {}),
"gapped-circle": (
svg_drawers.SvgPathCircleDrawer,
{"size_ratio": Decimal(0.8)},
),
"gapped-square": (
svg_drawers.SvgPathSquareDrawer,
{"size_ratio": Decimal(0.8)},
),
}
def __init__(self, *args, **kwargs):
self._subpaths: list[str] = []
super().__init__(*args, **kwargs)
def _svg(self, viewBox=None, **kwargs):
if viewBox is None:
dimension = self.units(self.pixel_size, text=False)
viewBox = "0 0 {d} {d}".format(d=dimension)
return super()._svg(viewBox=viewBox, **kwargs)
def process(self):
# Store the path just in case someone wants to use it again or in some
# unique way.
self.path = ET.Element(
ET.QName("path"), # type: ignore
d="".join(self._subpaths),
id="qr-path",
**self.QR_PATH_STYLE,
)
self._subpaths = []
self._img.append(self.path)
class SvgFillImage(SvgImage):
"""
An SvgImage that fills the background to white.
"""
background = "white"
class SvgPathFillImage(SvgPathImage):
"""
An SvgPathImage that fills the background to white.
"""
background = "white"