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,223 @@
# Store all kinds of lookup table.
# # generate rsPoly lookup table.
# from qrcode import base
# def create_bytes(rs_blocks):
# for r in range(len(rs_blocks)):
# dcCount = rs_blocks[r].data_count
# ecCount = rs_blocks[r].total_count - dcCount
# rsPoly = base.Polynomial([1], 0)
# for i in range(ecCount):
# rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0)
# return ecCount, rsPoly
# rsPoly_LUT = {}
# for version in range(1,41):
# for error_correction in range(4):
# rs_blocks_list = base.rs_blocks(version, error_correction)
# ecCount, rsPoly = create_bytes(rs_blocks_list)
# rsPoly_LUT[ecCount]=rsPoly.num
# print(rsPoly_LUT)
# Result. Usage: input: ecCount, output: Polynomial.num
# e.g. rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0)
rsPoly_LUT = {
7: [1, 127, 122, 154, 164, 11, 68, 117],
10: [1, 216, 194, 159, 111, 199, 94, 95, 113, 157, 193],
13: [1, 137, 73, 227, 17, 177, 17, 52, 13, 46, 43, 83, 132, 120],
15: [1, 29, 196, 111, 163, 112, 74, 10, 105, 105, 139, 132, 151, 32, 134, 26],
16: [1, 59, 13, 104, 189, 68, 209, 30, 8, 163, 65, 41, 229, 98, 50, 36, 59],
17: [1, 119, 66, 83, 120, 119, 22, 197, 83, 249, 41, 143, 134, 85, 53, 125, 99, 79],
18: [
1,
239,
251,
183,
113,
149,
175,
199,
215,
240,
220,
73,
82,
173,
75,
32,
67,
217,
146,
],
20: [
1,
152,
185,
240,
5,
111,
99,
6,
220,
112,
150,
69,
36,
187,
22,
228,
198,
121,
121,
165,
174,
],
22: [
1,
89,
179,
131,
176,
182,
244,
19,
189,
69,
40,
28,
137,
29,
123,
67,
253,
86,
218,
230,
26,
145,
245,
],
24: [
1,
122,
118,
169,
70,
178,
237,
216,
102,
115,
150,
229,
73,
130,
72,
61,
43,
206,
1,
237,
247,
127,
217,
144,
117,
],
26: [
1,
246,
51,
183,
4,
136,
98,
199,
152,
77,
56,
206,
24,
145,
40,
209,
117,
233,
42,
135,
68,
70,
144,
146,
77,
43,
94,
],
28: [
1,
252,
9,
28,
13,
18,
251,
208,
150,
103,
174,
100,
41,
167,
12,
247,
56,
117,
119,
233,
127,
181,
100,
121,
147,
176,
74,
58,
197,
],
30: [
1,
212,
246,
77,
73,
195,
192,
75,
98,
5,
70,
103,
177,
22,
217,
138,
51,
181,
246,
72,
25,
18,
46,
228,
74,
216,
195,
11,
106,
130,
150,
],
}

View File

@@ -0,0 +1,30 @@
from qrcode.main import QRCode
from qrcode.main import make # noqa
from qrcode.constants import ( # noqa
ERROR_CORRECT_L,
ERROR_CORRECT_M,
ERROR_CORRECT_Q,
ERROR_CORRECT_H,
)
from qrcode import image # noqa
def run_example(data="http://www.lincolnloop.com", *args, **kwargs):
"""
Build an example QR Code and display it.
There's an even easier way than the code here though: just use the ``make``
shortcut.
"""
qr = QRCode(*args, **kwargs)
qr.add_data(data)
im = qr.make_image()
im.show()
if __name__ == "__main__": # pragma: no cover
import sys
run_example(*sys.argv[1:])

View File

@@ -0,0 +1,313 @@
from typing import NamedTuple
from qrcode import constants
EXP_TABLE = list(range(256))
LOG_TABLE = list(range(256))
for i in range(8):
EXP_TABLE[i] = 1 << i
for i in range(8, 256):
EXP_TABLE[i] = (
EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8]
)
for i in range(255):
LOG_TABLE[EXP_TABLE[i]] = i
RS_BLOCK_OFFSET = {
constants.ERROR_CORRECT_L: 0,
constants.ERROR_CORRECT_M: 1,
constants.ERROR_CORRECT_Q: 2,
constants.ERROR_CORRECT_H: 3,
}
RS_BLOCK_TABLE = (
# L
# M
# Q
# H
# 1
(1, 26, 19),
(1, 26, 16),
(1, 26, 13),
(1, 26, 9),
# 2
(1, 44, 34),
(1, 44, 28),
(1, 44, 22),
(1, 44, 16),
# 3
(1, 70, 55),
(1, 70, 44),
(2, 35, 17),
(2, 35, 13),
# 4
(1, 100, 80),
(2, 50, 32),
(2, 50, 24),
(4, 25, 9),
# 5
(1, 134, 108),
(2, 67, 43),
(2, 33, 15, 2, 34, 16),
(2, 33, 11, 2, 34, 12),
# 6
(2, 86, 68),
(4, 43, 27),
(4, 43, 19),
(4, 43, 15),
# 7
(2, 98, 78),
(4, 49, 31),
(2, 32, 14, 4, 33, 15),
(4, 39, 13, 1, 40, 14),
# 8
(2, 121, 97),
(2, 60, 38, 2, 61, 39),
(4, 40, 18, 2, 41, 19),
(4, 40, 14, 2, 41, 15),
# 9
(2, 146, 116),
(3, 58, 36, 2, 59, 37),
(4, 36, 16, 4, 37, 17),
(4, 36, 12, 4, 37, 13),
# 10
(2, 86, 68, 2, 87, 69),
(4, 69, 43, 1, 70, 44),
(6, 43, 19, 2, 44, 20),
(6, 43, 15, 2, 44, 16),
# 11
(4, 101, 81),
(1, 80, 50, 4, 81, 51),
(4, 50, 22, 4, 51, 23),
(3, 36, 12, 8, 37, 13),
# 12
(2, 116, 92, 2, 117, 93),
(6, 58, 36, 2, 59, 37),
(4, 46, 20, 6, 47, 21),
(7, 42, 14, 4, 43, 15),
# 13
(4, 133, 107),
(8, 59, 37, 1, 60, 38),
(8, 44, 20, 4, 45, 21),
(12, 33, 11, 4, 34, 12),
# 14
(3, 145, 115, 1, 146, 116),
(4, 64, 40, 5, 65, 41),
(11, 36, 16, 5, 37, 17),
(11, 36, 12, 5, 37, 13),
# 15
(5, 109, 87, 1, 110, 88),
(5, 65, 41, 5, 66, 42),
(5, 54, 24, 7, 55, 25),
(11, 36, 12, 7, 37, 13),
# 16
(5, 122, 98, 1, 123, 99),
(7, 73, 45, 3, 74, 46),
(15, 43, 19, 2, 44, 20),
(3, 45, 15, 13, 46, 16),
# 17
(1, 135, 107, 5, 136, 108),
(10, 74, 46, 1, 75, 47),
(1, 50, 22, 15, 51, 23),
(2, 42, 14, 17, 43, 15),
# 18
(5, 150, 120, 1, 151, 121),
(9, 69, 43, 4, 70, 44),
(17, 50, 22, 1, 51, 23),
(2, 42, 14, 19, 43, 15),
# 19
(3, 141, 113, 4, 142, 114),
(3, 70, 44, 11, 71, 45),
(17, 47, 21, 4, 48, 22),
(9, 39, 13, 16, 40, 14),
# 20
(3, 135, 107, 5, 136, 108),
(3, 67, 41, 13, 68, 42),
(15, 54, 24, 5, 55, 25),
(15, 43, 15, 10, 44, 16),
# 21
(4, 144, 116, 4, 145, 117),
(17, 68, 42),
(17, 50, 22, 6, 51, 23),
(19, 46, 16, 6, 47, 17),
# 22
(2, 139, 111, 7, 140, 112),
(17, 74, 46),
(7, 54, 24, 16, 55, 25),
(34, 37, 13),
# 23
(4, 151, 121, 5, 152, 122),
(4, 75, 47, 14, 76, 48),
(11, 54, 24, 14, 55, 25),
(16, 45, 15, 14, 46, 16),
# 24
(6, 147, 117, 4, 148, 118),
(6, 73, 45, 14, 74, 46),
(11, 54, 24, 16, 55, 25),
(30, 46, 16, 2, 47, 17),
# 25
(8, 132, 106, 4, 133, 107),
(8, 75, 47, 13, 76, 48),
(7, 54, 24, 22, 55, 25),
(22, 45, 15, 13, 46, 16),
# 26
(10, 142, 114, 2, 143, 115),
(19, 74, 46, 4, 75, 47),
(28, 50, 22, 6, 51, 23),
(33, 46, 16, 4, 47, 17),
# 27
(8, 152, 122, 4, 153, 123),
(22, 73, 45, 3, 74, 46),
(8, 53, 23, 26, 54, 24),
(12, 45, 15, 28, 46, 16),
# 28
(3, 147, 117, 10, 148, 118),
(3, 73, 45, 23, 74, 46),
(4, 54, 24, 31, 55, 25),
(11, 45, 15, 31, 46, 16),
# 29
(7, 146, 116, 7, 147, 117),
(21, 73, 45, 7, 74, 46),
(1, 53, 23, 37, 54, 24),
(19, 45, 15, 26, 46, 16),
# 30
(5, 145, 115, 10, 146, 116),
(19, 75, 47, 10, 76, 48),
(15, 54, 24, 25, 55, 25),
(23, 45, 15, 25, 46, 16),
# 31
(13, 145, 115, 3, 146, 116),
(2, 74, 46, 29, 75, 47),
(42, 54, 24, 1, 55, 25),
(23, 45, 15, 28, 46, 16),
# 32
(17, 145, 115),
(10, 74, 46, 23, 75, 47),
(10, 54, 24, 35, 55, 25),
(19, 45, 15, 35, 46, 16),
# 33
(17, 145, 115, 1, 146, 116),
(14, 74, 46, 21, 75, 47),
(29, 54, 24, 19, 55, 25),
(11, 45, 15, 46, 46, 16),
# 34
(13, 145, 115, 6, 146, 116),
(14, 74, 46, 23, 75, 47),
(44, 54, 24, 7, 55, 25),
(59, 46, 16, 1, 47, 17),
# 35
(12, 151, 121, 7, 152, 122),
(12, 75, 47, 26, 76, 48),
(39, 54, 24, 14, 55, 25),
(22, 45, 15, 41, 46, 16),
# 36
(6, 151, 121, 14, 152, 122),
(6, 75, 47, 34, 76, 48),
(46, 54, 24, 10, 55, 25),
(2, 45, 15, 64, 46, 16),
# 37
(17, 152, 122, 4, 153, 123),
(29, 74, 46, 14, 75, 47),
(49, 54, 24, 10, 55, 25),
(24, 45, 15, 46, 46, 16),
# 38
(4, 152, 122, 18, 153, 123),
(13, 74, 46, 32, 75, 47),
(48, 54, 24, 14, 55, 25),
(42, 45, 15, 32, 46, 16),
# 39
(20, 147, 117, 4, 148, 118),
(40, 75, 47, 7, 76, 48),
(43, 54, 24, 22, 55, 25),
(10, 45, 15, 67, 46, 16),
# 40
(19, 148, 118, 6, 149, 119),
(18, 75, 47, 31, 76, 48),
(34, 54, 24, 34, 55, 25),
(20, 45, 15, 61, 46, 16),
)
def glog(n):
if n < 1: # pragma: no cover
raise ValueError(f"glog({n})")
return LOG_TABLE[n]
def gexp(n):
return EXP_TABLE[n % 255]
class Polynomial:
def __init__(self, num, shift):
if not num: # pragma: no cover
raise Exception(f"{len(num)}/{shift}")
offset = 0
for offset in range(len(num)):
if num[offset] != 0:
break
self.num = num[offset:] + [0] * shift
def __getitem__(self, index):
return self.num[index]
def __iter__(self):
return iter(self.num)
def __len__(self):
return len(self.num)
def __mul__(self, other):
num = [0] * (len(self) + len(other) - 1)
for i, item in enumerate(self):
for j, other_item in enumerate(other):
num[i + j] ^= gexp(glog(item) + glog(other_item))
return Polynomial(num, 0)
def __mod__(self, other):
difference = len(self) - len(other)
if difference < 0:
return self
ratio = glog(self[0]) - glog(other[0])
num = [
item ^ gexp(glog(other_item) + ratio)
for item, other_item in zip(self, other)
]
if difference:
num.extend(self[-difference:])
# recursive call
return Polynomial(num, 0) % other
class RSBlock(NamedTuple):
total_count: int
data_count: int
def rs_blocks(version, error_correction):
if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover
raise Exception(
"bad rs block @ version: %s / error_correction: %s"
% (version, error_correction)
)
offset = RS_BLOCK_OFFSET[error_correction]
rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset]
blocks = []
for i in range(0, len(rs_block), 3):
count, total_count, data_count = rs_block[i : i + 3]
for _ in range(count):
blocks.append(RSBlock(total_count, data_count))
return blocks

View File

@@ -0,0 +1,4 @@
try:
import lxml.etree as ET # type: ignore # noqa: F401
except ImportError:
import xml.etree.ElementTree as ET # type: ignore # noqa: F401

View File

@@ -0,0 +1,7 @@
# Try to import png library.
PngWriter = None
try:
from png import Writer as PngWriter # type: ignore # noqa: F401
except ImportError: # pragma: no cover
pass

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python
"""
qr - Convert stdin (or the first argument) to a QR Code.
When stdout is a tty the QR Code is printed to the terminal and when stdout is
a pipe to a file an image is written. The default image format is PNG.
"""
import optparse
import os
import sys
from typing import NoReturn, Optional
from collections.abc import Iterable
from importlib import metadata
import qrcode
from qrcode.image.base import BaseImage, DrawerAliases
# The next block is added to get the terminal to display properly on MS platforms
if sys.platform.startswith(("win", "cygwin")): # pragma: no cover
import colorama # type: ignore
colorama.init()
default_factories = {
"pil": "qrcode.image.pil.PilImage",
"png": "qrcode.image.pure.PyPNGImage",
"svg": "qrcode.image.svg.SvgImage",
"svg-fragment": "qrcode.image.svg.SvgFragmentImage",
"svg-path": "qrcode.image.svg.SvgPathImage",
# Keeping for backwards compatibility:
"pymaging": "qrcode.image.pure.PymagingImage",
}
error_correction = {
"L": qrcode.ERROR_CORRECT_L,
"M": qrcode.ERROR_CORRECT_M,
"Q": qrcode.ERROR_CORRECT_Q,
"H": qrcode.ERROR_CORRECT_H,
}
def main(args=None):
if args is None:
args = sys.argv[1:]
version = metadata.version("qrcode")
parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version)
# Wrap parser.error in a typed NoReturn method for better typing.
def raise_error(msg: str) -> NoReturn:
parser.error(msg)
raise # pragma: no cover
parser.add_option(
"--factory",
help="Full python path to the image factory class to "
"create the image with. You can use the following shortcuts to the "
f"built-in image factory classes: {commas(default_factories)}.",
)
parser.add_option(
"--factory-drawer",
help=f"Use an alternate drawer. {get_drawer_help()}.",
)
parser.add_option(
"--optimize",
type=int,
help="Optimize the data by looking for chunks "
"of at least this many characters that could use a more efficient "
"encoding method. Use 0 to turn off chunk optimization.",
)
parser.add_option(
"--error-correction",
type="choice",
choices=sorted(error_correction.keys()),
default="M",
help="The error correction level to use. Choices are L (7%), "
"M (15%, default), Q (25%), and H (30%).",
)
parser.add_option(
"--ascii", help="Print as ascii even if stdout is piped.", action="store_true"
)
parser.add_option(
"--output",
help="The output file. If not specified, the image is sent to "
"the standard output.",
)
opts, args = parser.parse_args(args)
if opts.factory:
module = default_factories.get(opts.factory, opts.factory)
try:
image_factory = get_factory(module)
except ValueError as e:
raise_error(str(e))
else:
image_factory = None
qr = qrcode.QRCode(
error_correction=error_correction[opts.error_correction],
image_factory=image_factory,
)
if args:
data = args[0]
data = data.encode(errors="surrogateescape")
else:
data = sys.stdin.buffer.read()
if opts.optimize is None:
qr.add_data(data)
else:
qr.add_data(data, optimize=opts.optimize)
if opts.output:
img = qr.make_image()
with open(opts.output, "wb") as out:
img.save(out)
else:
if image_factory is None and (os.isatty(sys.stdout.fileno()) or opts.ascii):
qr.print_ascii(tty=not opts.ascii)
return
kwargs = {}
aliases: Optional[DrawerAliases] = getattr(
qr.image_factory, "drawer_aliases", None
)
if opts.factory_drawer:
if not aliases:
raise_error("The selected factory has no drawer aliases.")
if opts.factory_drawer not in aliases:
raise_error(
f"{opts.factory_drawer} factory drawer not found."
f" Expected {commas(aliases)}"
)
drawer_cls, drawer_kwargs = aliases[opts.factory_drawer]
kwargs["module_drawer"] = drawer_cls(**drawer_kwargs)
img = qr.make_image(**kwargs)
sys.stdout.flush()
img.save(sys.stdout.buffer)
def get_factory(module: str) -> type[BaseImage]:
if "." not in module:
raise ValueError("The image factory is not a full python path")
module, name = module.rsplit(".", 1)
imp = __import__(module, {}, {}, [name])
return getattr(imp, name)
def get_drawer_help() -> str:
help: dict[str, set] = {}
for alias, module in default_factories.items():
try:
image = get_factory(module)
except ImportError: # pragma: no cover
continue
aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None)
if not aliases:
continue
factories = help.setdefault(commas(aliases), set())
factories.add(alias)
return ". ".join(
f"For {commas(factories, 'and')}, use: {aliases}"
for aliases, factories in help.items()
)
def commas(items: Iterable[str], joiner="or") -> str:
items = tuple(items)
if not items:
return ""
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])} {joiner} {items[-1]}"
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,5 @@
# QR error correct levels
ERROR_CORRECT_L = 1
ERROR_CORRECT_M = 0
ERROR_CORRECT_Q = 3
ERROR_CORRECT_H = 2

View File

@@ -0,0 +1,2 @@
class DataOverflowError(Exception):
pass

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"

View File

@@ -0,0 +1,541 @@
import sys
from bisect import bisect_left
from typing import (
Generic,
NamedTuple,
Optional,
TypeVar,
cast,
overload,
Literal,
)
from qrcode import constants, exceptions, util
from qrcode.image.base import BaseImage
from qrcode.image.pure import PyPNGImage
ModulesType = list[list[Optional[bool]]]
# Cache modules generated just based on the QR Code version
precomputed_qr_blanks: dict[int, ModulesType] = {}
def make(data=None, **kwargs):
qr = QRCode(**kwargs)
qr.add_data(data)
return qr.make_image()
def _check_box_size(size):
if int(size) <= 0:
raise ValueError(f"Invalid box size (was {size}, expected larger than 0)")
def _check_border(size):
if int(size) < 0:
raise ValueError(
"Invalid border value (was %s, expected 0 or larger than that)" % size
)
def _check_mask_pattern(mask_pattern):
if mask_pattern is None:
return
if not isinstance(mask_pattern, int):
raise TypeError(
f"Invalid mask pattern (was {type(mask_pattern)}, expected int)"
)
if mask_pattern < 0 or mask_pattern > 7:
raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})")
def copy_2d_array(x):
return [row[:] for row in x]
class ActiveWithNeighbors(NamedTuple):
NW: bool
N: bool
NE: bool
W: bool
me: bool
E: bool
SW: bool
S: bool
SE: bool
def __bool__(self) -> bool:
return self.me
GenericImage = TypeVar("GenericImage", bound=BaseImage)
GenericImageLocal = TypeVar("GenericImageLocal", bound=BaseImage)
class QRCode(Generic[GenericImage]):
modules: ModulesType
_version: Optional[int] = None
def __init__(
self,
version=None,
error_correction=constants.ERROR_CORRECT_M,
box_size=10,
border=4,
image_factory: Optional[type[GenericImage]] = None,
mask_pattern=None,
):
_check_box_size(box_size)
_check_border(border)
self.version = version
self.error_correction = int(error_correction)
self.box_size = int(box_size)
# Spec says border should be at least four boxes wide, but allow for
# any (e.g. for producing printable QR codes).
self.border = int(border)
self.mask_pattern = mask_pattern
self.image_factory = image_factory
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
self.clear()
@property
def version(self) -> int:
if self._version is None:
self.best_fit()
return cast(int, self._version)
@version.setter
def version(self, value) -> None:
if value is not None:
value = int(value)
util.check_version(value)
self._version = value
@property
def mask_pattern(self):
return self._mask_pattern
@mask_pattern.setter
def mask_pattern(self, pattern):
_check_mask_pattern(pattern)
self._mask_pattern = pattern
def clear(self):
"""
Reset the internal data.
"""
self.modules = [[]]
self.modules_count = 0
self.data_cache = None
self.data_list = []
def add_data(self, data, optimize=20):
"""
Add data to this QR Code.
:param optimize: Data will be split into multiple chunks to optimize
the QR size by finding to more compressed modes of at least this
length. Set to ``0`` to avoid optimizing at all.
"""
if isinstance(data, util.QRData):
self.data_list.append(data)
elif optimize:
self.data_list.extend(util.optimal_data_chunks(data, minimum=optimize))
else:
self.data_list.append(util.QRData(data))
self.data_cache = None
def make(self, fit=True):
"""
Compile the data into a QR Code array.
:param fit: If ``True`` (or if a size has not been provided), find the
best fit for the data to avoid data overflow errors.
"""
if fit or (self.version is None):
self.best_fit(start=self.version)
if self.mask_pattern is None:
self.makeImpl(False, self.best_mask_pattern())
else:
self.makeImpl(False, self.mask_pattern)
def makeImpl(self, test, mask_pattern):
self.modules_count = self.version * 4 + 17
if self.version in precomputed_qr_blanks:
self.modules = copy_2d_array(precomputed_qr_blanks[self.version])
else:
self.modules = [
[None] * self.modules_count for i in range(self.modules_count)
]
self.setup_position_probe_pattern(0, 0)
self.setup_position_probe_pattern(self.modules_count - 7, 0)
self.setup_position_probe_pattern(0, self.modules_count - 7)
self.setup_position_adjust_pattern()
self.setup_timing_pattern()
precomputed_qr_blanks[self.version] = copy_2d_array(self.modules)
self.setup_type_info(test, mask_pattern)
if self.version >= 7:
self.setup_type_number(test)
if self.data_cache is None:
self.data_cache = util.create_data(
self.version, self.error_correction, self.data_list
)
self.map_data(self.data_cache, mask_pattern)
def setup_position_probe_pattern(self, row, col):
for r in range(-1, 8):
if row + r <= -1 or self.modules_count <= row + r:
continue
for c in range(-1, 8):
if col + c <= -1 or self.modules_count <= col + c:
continue
if (
(0 <= r <= 6 and c in {0, 6})
or (0 <= c <= 6 and r in {0, 6})
or (2 <= r <= 4 and 2 <= c <= 4)
):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def best_fit(self, start=None):
"""
Find the minimum size required to fit in the data.
"""
if start is None:
start = 1
util.check_version(start)
# Corresponds to the code in util.create_data, except we don't yet know
# version, so optimistically assume start and check later
mode_sizes = util.mode_sizes_for_version(start)
buffer = util.BitBuffer()
for data in self.data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), mode_sizes[data.mode])
data.write(buffer)
needed_bits = len(buffer)
self.version = bisect_left(
util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start
)
if self.version == 41:
raise exceptions.DataOverflowError()
# Now check whether we need more bits for the mode sizes, recursing if
# our guess was too low
if mode_sizes is not util.mode_sizes_for_version(self.version):
self.best_fit(start=self.version)
return self.version
def best_mask_pattern(self):
"""
Find the most efficient mask pattern.
"""
min_lost_point = 0
pattern = 0
for i in range(8):
self.makeImpl(True, i)
lost_point = util.lost_point(self.modules)
if i == 0 or min_lost_point > lost_point:
min_lost_point = lost_point
pattern = i
return pattern
def print_tty(self, out=None):
"""
Output the QR Code only using TTY colors.
If the data has not been compiled yet, make it first.
"""
if out is None:
import sys
out = sys.stdout
if not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
for r in range(modcount):
out.write("\x1b[1;47m \x1b[40m")
for c in range(modcount):
if self.modules[r][c]:
out.write(" ")
else:
out.write("\x1b[1;47m \x1b[40m")
out.write("\x1b[1;47m \x1b[0m\n")
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
out.flush()
def print_ascii(self, out=None, tty=False, invert=False):
"""
Output the QR Code using ASCII characters.
:param tty: use fixed TTY color codes (forces invert=True)
:param invert: invert the ASCII characters (solid <-> transparent)
"""
if out is None:
out = sys.stdout
if tty and not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
codes = [bytes((code,)).decode("cp437") for code in (255, 223, 220, 219)]
if tty:
invert = True
if invert:
codes.reverse()
def get_module(x, y) -> int:
if invert and self.border and max(x, y) >= modcount + self.border:
return 1
if min(x, y) < 0 or max(x, y) >= modcount:
return 0
return cast(int, self.modules[x][y])
for r in range(-self.border, modcount + self.border, 2):
if tty:
if not invert or r < modcount + self.border - 1:
out.write("\x1b[48;5;232m") # Background black
out.write("\x1b[38;5;255m") # Foreground white
for c in range(-self.border, modcount + self.border):
pos = get_module(r, c) + (get_module(r + 1, c) << 1)
out.write(codes[pos])
if tty:
out.write("\x1b[0m")
out.write("\n")
out.flush()
@overload
def make_image(
self, image_factory: Literal[None] = None, **kwargs
) -> GenericImage: ...
@overload
def make_image(
self, image_factory: type[GenericImageLocal] = None, **kwargs
) -> GenericImageLocal: ...
def make_image(self, image_factory=None, **kwargs):
"""
Make an image from the QR Code data.
If the data has not been compiled yet, make it first.
"""
# allow embeded_ parameters with typos for backwards compatibility
if (
kwargs.get("embedded_image_path")
or kwargs.get("embedded_image")
or kwargs.get("embeded_image_path")
or kwargs.get("embeded_image")
) and self.error_correction != constants.ERROR_CORRECT_H:
raise ValueError(
"Error correction level must be ERROR_CORRECT_H if an embedded image is provided"
)
_check_box_size(self.box_size)
if self.data_cache is None:
self.make()
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
else:
image_factory = self.image_factory
if image_factory is None:
from qrcode.image.pil import Image, PilImage
# Use PIL by default if available, otherwise use PyPNG.
image_factory = PilImage if Image else PyPNGImage
im = image_factory(
self.border,
self.modules_count,
self.box_size,
qrcode_modules=self.modules,
**kwargs,
)
if im.needs_drawrect:
for r in range(self.modules_count):
for c in range(self.modules_count):
if im.needs_context:
im.drawrect_context(r, c, qr=self)
elif self.modules[r][c]:
im.drawrect(r, c)
if im.needs_processing:
im.process()
return im
# return true if and only if (row, col) is in the module
def is_constrained(self, row: int, col: int) -> bool:
return (
row >= 0
and row < len(self.modules)
and col >= 0
and col < len(self.modules[row])
)
def setup_timing_pattern(self):
for r in range(8, self.modules_count - 8):
if self.modules[r][6] is not None:
continue
self.modules[r][6] = r % 2 == 0
for c in range(8, self.modules_count - 8):
if self.modules[6][c] is not None:
continue
self.modules[6][c] = c % 2 == 0
def setup_position_adjust_pattern(self):
pos = util.pattern_position(self.version)
for i in range(len(pos)):
row = pos[i]
for j in range(len(pos)):
col = pos[j]
if self.modules[row][col] is not None:
continue
for r in range(-2, 3):
for c in range(-2, 3):
if (
r == -2
or r == 2
or c == -2
or c == 2
or (r == 0 and c == 0)
):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def setup_type_number(self, test):
bits = util.BCH_type_number(self.version)
for i in range(18):
mod = not test and ((bits >> i) & 1) == 1
self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod
for i in range(18):
mod = not test and ((bits >> i) & 1) == 1
self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod
def setup_type_info(self, test, mask_pattern):
data = (self.error_correction << 3) | mask_pattern
bits = util.BCH_type_info(data)
# vertical
for i in range(15):
mod = not test and ((bits >> i) & 1) == 1
if i < 6:
self.modules[i][8] = mod
elif i < 8:
self.modules[i + 1][8] = mod
else:
self.modules[self.modules_count - 15 + i][8] = mod
# horizontal
for i in range(15):
mod = not test and ((bits >> i) & 1) == 1
if i < 8:
self.modules[8][self.modules_count - i - 1] = mod
elif i < 9:
self.modules[8][15 - i - 1 + 1] = mod
else:
self.modules[8][15 - i - 1] = mod
# fixed module
self.modules[self.modules_count - 8][8] = not test
def map_data(self, data, mask_pattern):
inc = -1
row = self.modules_count - 1
bitIndex = 7
byteIndex = 0
mask_func = util.mask_func(mask_pattern)
data_len = len(data)
for col in range(self.modules_count - 1, 0, -2):
if col <= 6:
col -= 1
col_range = (col, col - 1)
while True:
for c in col_range:
if self.modules[row][c] is None:
dark = False
if byteIndex < data_len:
dark = ((data[byteIndex] >> bitIndex) & 1) == 1
if mask_func(row, c):
dark = not dark
self.modules[row][c] = dark
bitIndex -= 1
if bitIndex == -1:
byteIndex += 1
bitIndex = 7
row += inc
if row < 0 or self.modules_count <= row:
row -= inc
inc = -inc
break
def get_matrix(self):
"""
Return the QR Code as a multidimensional array, including the border.
To return the array without a border, set ``self.border`` to 0 first.
"""
if self.data_cache is None:
self.make()
if not self.border:
return self.modules
width = len(self.modules) + self.border * 2
code = [[False] * width] * self.border
x_border = [False] * self.border
for module in self.modules:
code.append(x_border + cast(list[bool], module) + x_border)
code += [[False] * width] * self.border
return code
def active_with_neighbors(self, row: int, col: int) -> ActiveWithNeighbors:
context: list[bool] = []
for r in range(row - 1, row + 2):
for c in range(col - 1, col + 2):
context.append(self.is_constrained(r, c) and bool(self.modules[r][c]))
return ActiveWithNeighbors(*context)

View File

@@ -0,0 +1,42 @@
"""
This file provides zest.releaser entrypoints using when releasing new
qrcode versions.
"""
import os
import re
import datetime
def update_manpage(data):
"""
Update the version in the manpage document.
"""
if data["name"] != "qrcode":
return
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
filename = os.path.join(base_dir, "doc", "qr.1")
with open(filename) as f:
lines = f.readlines()
changed = False
for i, line in enumerate(lines):
if not line.startswith(".TH "):
continue
parts = re.split(r'"([^"]*)"', line)
if len(parts) < 5:
continue
changed = parts[3] != data["new_version"]
if changed:
# Update version
parts[3] = data["new_version"]
# Update date
parts[1] = datetime.datetime.now().strftime("%-d %b %Y")
lines[i] = '"'.join(parts)
break
if changed:
with open(filename, "w") as f:
for line in lines:
f.write(line)

View File

@@ -0,0 +1,4 @@
UNICODE_TEXT = "\u03b1\u03b2\u03b3"
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)

View File

@@ -0,0 +1,13 @@
from unittest import mock
import pytest
from qrcode import run_example
pytest.importorskip("PIL", reason="Requires PIL")
@mock.patch("PIL.Image.Image.show")
def test_example(mock_show):
run_example()
mock_show.assert_called_with()

View File

@@ -0,0 +1,271 @@
import io
from unittest import mock
import pytest
import qrcode
import qrcode.util
from qrcode.exceptions import DataOverflowError
from qrcode.image.base import BaseImage
from qrcode.tests.consts import UNICODE_TEXT
from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData
def test_basic():
qr = qrcode.QRCode(version=1)
qr.add_data("a")
qr.make(fit=False)
def test_large():
qr = qrcode.QRCode(version=27)
qr.add_data("a")
qr.make(fit=False)
def test_invalid_version():
with pytest.raises(ValueError):
qrcode.QRCode(version=42)
def test_invalid_border():
with pytest.raises(ValueError):
qrcode.QRCode(border=-1)
def test_overflow():
qr = qrcode.QRCode(version=1)
qr.add_data("abcdefghijklmno")
with pytest.raises(DataOverflowError):
qr.make(fit=False)
def test_add_qrdata():
qr = qrcode.QRCode(version=1)
data = QRData("a")
qr.add_data(data)
qr.make(fit=False)
def test_fit():
qr = qrcode.QRCode()
qr.add_data("a")
qr.make()
assert qr.version == 1
qr.add_data("bcdefghijklmno")
qr.make()
assert qr.version == 2
def test_mode_number():
qr = qrcode.QRCode()
qr.add_data("1234567890123456789012345678901234", optimize=0)
qr.make()
assert qr.version == 1
assert qr.data_list[0].mode == MODE_NUMBER
def test_mode_alpha():
qr = qrcode.QRCode()
qr.add_data("ABCDEFGHIJ1234567890", optimize=0)
qr.make()
assert qr.version == 1
assert qr.data_list[0].mode == MODE_ALPHA_NUM
def test_regression_mode_comma():
qr = qrcode.QRCode()
qr.add_data(",", optimize=0)
qr.make()
assert qr.data_list[0].mode == MODE_8BIT_BYTE
def test_mode_8bit():
qr = qrcode.QRCode()
qr.add_data("abcABC" + UNICODE_TEXT, optimize=0)
qr.make()
assert qr.version == 1
assert qr.data_list[0].mode == MODE_8BIT_BYTE
def test_mode_8bit_newline():
qr = qrcode.QRCode()
qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0)
qr.make()
assert qr.data_list[0].mode == MODE_8BIT_BYTE
def test_make_image_with_wrong_pattern():
with pytest.raises(TypeError):
qrcode.QRCode(mask_pattern="string pattern")
with pytest.raises(ValueError):
qrcode.QRCode(mask_pattern=-1)
with pytest.raises(ValueError):
qrcode.QRCode(mask_pattern=42)
def test_mask_pattern_setter():
qr = qrcode.QRCode()
with pytest.raises(TypeError):
qr.mask_pattern = "string pattern"
with pytest.raises(ValueError):
qr.mask_pattern = -1
with pytest.raises(ValueError):
qr.mask_pattern = 8
def test_qrcode_bad_factory():
with pytest.raises(TypeError):
qrcode.QRCode(image_factory="not_BaseImage") # type: ignore
with pytest.raises(AssertionError):
qrcode.QRCode(image_factory=dict) # type: ignore
def test_qrcode_factory():
class MockFactory(BaseImage):
drawrect = mock.Mock()
new_image = mock.Mock()
qr = qrcode.QRCode(image_factory=MockFactory)
qr.add_data(UNICODE_TEXT)
qr.make_image()
assert MockFactory.new_image.called
assert MockFactory.drawrect.called
def test_optimize():
qr = qrcode.QRCode()
text = "A1abc12345def1HELLOa"
qr.add_data(text, optimize=4)
qr.make()
assert [d.mode for d in qr.data_list] == [
MODE_8BIT_BYTE,
MODE_NUMBER,
MODE_8BIT_BYTE,
MODE_ALPHA_NUM,
MODE_8BIT_BYTE,
]
assert qr.version == 2
def test_optimize_short():
qr = qrcode.QRCode()
text = "A1abc1234567def1HELLOa"
qr.add_data(text, optimize=7)
qr.make()
assert len(qr.data_list) == 3
assert [d.mode for d in qr.data_list] == [
MODE_8BIT_BYTE,
MODE_NUMBER,
MODE_8BIT_BYTE,
]
assert qr.version == 2
def test_optimize_longer_than_data():
qr = qrcode.QRCode()
text = "ABCDEFGHIJK"
qr.add_data(text, optimize=12)
assert len(qr.data_list) == 1
assert qr.data_list[0].mode == MODE_ALPHA_NUM
def test_optimize_size():
text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5
qr = qrcode.QRCode()
qr.add_data(text)
qr.make()
assert qr.version == 10
qr = qrcode.QRCode()
qr.add_data(text, optimize=0)
qr.make()
assert qr.version == 11
def test_qrdata_repr():
data = b"hello"
data_obj = qrcode.util.QRData(data)
assert repr(data_obj) == repr(data)
def test_print_ascii_stdout():
qr = qrcode.QRCode()
with mock.patch("sys.stdout") as fake_stdout:
fake_stdout.isatty.return_value = None
with pytest.raises(OSError):
qr.print_ascii(tty=True)
assert fake_stdout.isatty.called
def test_print_ascii():
qr = qrcode.QRCode(border=0)
f = io.StringIO()
qr.print_ascii(out=f)
printed = f.getvalue()
f.close()
expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588"
assert printed[: len(expected)] == expected
f = io.StringIO()
f.isatty = lambda: True
qr.print_ascii(out=f, tty=True)
printed = f.getvalue()
f.close()
expected = "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0"
assert printed[: len(expected)] == expected
def test_print_tty_stdout():
qr = qrcode.QRCode()
with mock.patch("sys.stdout") as fake_stdout:
fake_stdout.isatty.return_value = None
pytest.raises(OSError, qr.print_tty)
assert fake_stdout.isatty.called
def test_print_tty():
qr = qrcode.QRCode()
f = io.StringIO()
f.isatty = lambda: True
qr.print_tty(out=f)
printed = f.getvalue()
f.close()
BOLD_WHITE_BG = "\x1b[1;47m"
BLACK_BG = "\x1b[40m"
WHITE_BLOCK = BOLD_WHITE_BG + " " + BLACK_BG
EOL = "\x1b[0m\n"
expected = BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK
assert printed[: len(expected)] == expected
def test_get_matrix():
qr = qrcode.QRCode(border=0)
qr.add_data("1")
assert qr.get_matrix() == qr.modules
def test_get_matrix_border():
qr = qrcode.QRCode(border=1)
qr.add_data("1")
matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]]
assert matrix == qr.modules
def test_negative_size_at_construction():
with pytest.raises(ValueError):
qrcode.QRCode(box_size=-1)
def test_negative_size_at_usage():
qr = qrcode.QRCode()
qr.box_size = -1
with pytest.raises(ValueError):
qr.make_image()

View File

@@ -0,0 +1,157 @@
import io
import pytest
import qrcode
import qrcode.util
from qrcode.tests.consts import BLACK, RED, UNICODE_TEXT, WHITE
Image = pytest.importorskip("PIL.Image", reason="PIL is not installed")
if Image:
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles import colormasks, moduledrawers
def test_render_pil():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image()
img.save(io.BytesIO())
assert isinstance(img.get_image(), Image.Image)
@pytest.mark.parametrize("back_color", ["TransParent", "red", (255, 195, 235)])
def test_render_pil_background(back_color):
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(back_color="TransParent")
img.save(io.BytesIO())
def test_render_pil_with_rgb_color_tuples():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35))
img.save(io.BytesIO())
def test_render_with_pattern():
qr = qrcode.QRCode(mask_pattern=3)
qr.add_data(UNICODE_TEXT)
img = qr.make_image()
img.save(io.BytesIO())
def test_render_styled_Image():
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L)
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=StyledPilImage)
img.save(io.BytesIO())
def test_render_styled_with_embedded_image():
embedded_img = Image.new("RGB", (10, 10), color="red")
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H)
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=StyledPilImage, embedded_image=embedded_img)
img.save(io.BytesIO())
def test_render_styled_with_embedded_image_path(tmp_path):
tmpfile = str(tmp_path / "test.png")
embedded_img = Image.new("RGB", (10, 10), color="red")
embedded_img.save(tmpfile)
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H)
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=StyledPilImage, embedded_image_path=tmpfile)
img.save(io.BytesIO())
@pytest.mark.parametrize(
"drawer",
[
moduledrawers.CircleModuleDrawer,
moduledrawers.GappedSquareModuleDrawer,
moduledrawers.HorizontalBarsDrawer,
moduledrawers.RoundedModuleDrawer,
moduledrawers.SquareModuleDrawer,
moduledrawers.VerticalBarsDrawer,
],
)
def test_render_styled_with_drawer(drawer):
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L)
qr.add_data(UNICODE_TEXT)
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=drawer(),
)
img.save(io.BytesIO())
@pytest.mark.parametrize(
"mask",
[
colormasks.SolidFillColorMask(),
colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED),
colormasks.SolidFillColorMask(back_color=(255, 0, 255, 255), front_color=RED),
colormasks.RadialGradiantColorMask(
back_color=WHITE, center_color=BLACK, edge_color=RED
),
colormasks.SquareGradiantColorMask(
back_color=WHITE, center_color=BLACK, edge_color=RED
),
colormasks.HorizontalGradiantColorMask(
back_color=WHITE, left_color=RED, right_color=BLACK
),
colormasks.VerticalGradiantColorMask(
back_color=WHITE, top_color=RED, bottom_color=BLACK
),
colormasks.ImageColorMask(
back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red")
),
],
)
def test_render_styled_with_mask(mask):
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L)
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=StyledPilImage, color_mask=mask)
img.save(io.BytesIO())
def test_embedded_image_and_error_correction(tmp_path):
"If an embedded image is specified, error correction must be the highest so the QR code is readable"
tmpfile = str(tmp_path / "test.png")
embedded_img = Image.new("RGB", (10, 10), color="red")
embedded_img.save(tmpfile)
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L)
qr.add_data(UNICODE_TEXT)
with pytest.raises(ValueError):
qr.make_image(embedded_image_path=tmpfile)
with pytest.raises(ValueError):
qr.make_image(embedded_image=embedded_img)
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_M)
qr.add_data(UNICODE_TEXT)
with pytest.raises(ValueError):
qr.make_image(embedded_image_path=tmpfile)
with pytest.raises(ValueError):
qr.make_image(embedded_image=embedded_img)
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_Q)
qr.add_data(UNICODE_TEXT)
with pytest.raises(ValueError):
qr.make_image(embedded_image_path=tmpfile)
with pytest.raises(ValueError):
qr.make_image(embedded_image=embedded_img)
# The only accepted correction level when an embedded image is provided
qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H)
qr.add_data(UNICODE_TEXT)
qr.make_image(embedded_image_path=tmpfile)
qr.make_image(embedded_image=embedded_img)
def test_shortcut():
qrcode.make("image")

View File

@@ -0,0 +1,35 @@
import io
from unittest import mock
import pytest
import qrcode
import qrcode.util
from qrcode.image.pure import PyPNGImage
from qrcode.tests.consts import UNICODE_TEXT
png = pytest.importorskip("png", reason="png is not installed")
def test_render_pypng():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=PyPNGImage)
assert isinstance(img.get_image(), png.Writer)
print(img.width, img.box_size, img.border)
img.save(io.BytesIO())
def test_render_pypng_to_str():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=PyPNGImage)
assert isinstance(img.get_image(), png.Writer)
mock_open = mock.mock_open()
with mock.patch("qrcode.image.pure.open", mock_open, create=True):
img.save("test_file.png")
mock_open.assert_called_once_with("test_file.png", "wb")
mock_open("test_file.png", "wb").write.assert_called()

View File

@@ -0,0 +1,54 @@
import io
import qrcode
from qrcode.image import svg
from qrcode.tests.consts import UNICODE_TEXT
class SvgImageWhite(svg.SvgImage):
background = "white"
def test_render_svg():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgImage)
img.save(io.BytesIO())
def test_render_svg_path():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgPathImage)
img.save(io.BytesIO())
def test_render_svg_fragment():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgFragmentImage)
img.save(io.BytesIO())
def test_svg_string():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgFragmentImage)
file_like = io.BytesIO()
img.save(file_like)
file_like.seek(0)
assert file_like.read() in img.to_string()
def test_render_svg_with_background():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=SvgImageWhite)
img.save(io.BytesIO())
def test_svg_circle_drawer():
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle")
img.save(io.BytesIO())

View File

@@ -0,0 +1,43 @@
import builtins
import datetime
import re
from unittest import mock
from qrcode.release import update_manpage
OPEN = f"{builtins.__name__}.open"
DATA = 'test\n.TH "date" "version" "description"\nthis'
@mock.patch(OPEN, new_callable=mock.mock_open, read_data=".TH invalid")
def test_invalid_data(mock_file):
update_manpage({"name": "qrcode", "new_version": "1.23"})
mock_file.assert_called()
mock_file().write.assert_not_called()
@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA)
def test_not_qrcode(mock_file):
update_manpage({"name": "not-qrcode"})
mock_file.assert_not_called()
@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA)
def test_no_change(mock_file):
update_manpage({"name": "qrcode", "new_version": "version"})
mock_file.assert_called()
mock_file().write.assert_not_called()
@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA)
def test_change(mock_file):
update_manpage({"name": "qrcode", "new_version": "3.11"})
expected = re.split(r"([^\n]*(?:\n|$))", DATA)[1::2]
expected[1] = (
expected[1]
.replace("version", "3.11")
.replace("date", datetime.datetime.now().strftime("%-d %b %Y"))
)
mock_file().write.assert_has_calls(
[mock.call(line) for line in expected if line != ""], any_order=True
)

View File

@@ -0,0 +1,97 @@
import sys
from unittest import mock
import pytest
from qrcode.console_scripts import commas, main
def bad_read():
raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte")
@mock.patch("os.isatty", lambda *args: True)
@mock.patch("qrcode.main.QRCode.print_ascii")
def test_isatty(mock_print_ascii):
main(["testtext"])
mock_print_ascii.assert_called_with(tty=True)
@mock.patch("os.isatty", lambda *args: False)
def test_piped():
pytest.importorskip("PIL", reason="Requires PIL")
main(["testtext"])
@mock.patch("os.isatty", lambda *args: True)
def test_stdin():
with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii:
with mock.patch("sys.stdin") as mock_stdin:
mock_stdin.buffer.read.return_value = "testtext"
main([])
assert mock_stdin.buffer.read.called
mock_print_ascii.assert_called_with(tty=True)
@mock.patch("os.isatty", lambda *args: True)
def test_stdin_py3_unicodedecodeerror():
with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii:
with mock.patch("sys.stdin") as mock_stdin:
mock_stdin.buffer.read.return_value = "testtext"
mock_stdin.read.side_effect = bad_read
# sys.stdin.read() will raise an error...
with pytest.raises(UnicodeDecodeError):
sys.stdin.read()
# ... but it won't be used now.
main([])
mock_print_ascii.assert_called_with(tty=True)
def test_optimize():
pytest.importorskip("PIL", reason="Requires PIL")
main("testtext --optimize 0".split())
def test_factory():
main(["testtext", "--factory", "svg"])
def test_bad_factory():
with pytest.raises(SystemExit):
main(["testtext", "--factory", "nope"])
@mock.patch.object(sys, "argv", "qr testtext output".split())
def test_sys_argv():
pytest.importorskip("PIL", reason="Requires PIL")
main()
def test_output(tmp_path):
pytest.importorskip("PIL", reason="Requires PIL")
main(["testtext", "--output", str(tmp_path / "test.png")])
def test_factory_drawer_none(capsys):
pytest.importorskip("PIL", reason="Requires PIL")
with pytest.raises(SystemExit):
main("testtext --factory pil --factory-drawer nope".split())
assert "The selected factory has no drawer aliases" in capsys.readouterr()[1]
def test_factory_drawer_bad(capsys):
with pytest.raises(SystemExit):
main("testtext --factory svg --factory-drawer sobad".split())
assert "sobad factory drawer not found" in capsys.readouterr()[1]
def test_factory_drawer(capsys):
main("testtext --factory svg --factory-drawer circle".split())
def test_commas():
assert commas([]) == ""
assert commas(["A"]) == "A"
assert commas("AB") == "A or B"
assert commas("ABC") == "A, B or C"
assert commas("ABC", joiner="and") == "A, B and C"

View File

@@ -0,0 +1,11 @@
import pytest
from qrcode import util
def test_check_wrong_version():
with pytest.raises(ValueError):
util.check_version(0)
with pytest.raises(ValueError):
util.check_version(41)

View File

@@ -0,0 +1,584 @@
import math
import re
from qrcode import LUT, base, exceptions
from qrcode.base import RSBlock
# QR encoding modes.
MODE_NUMBER = 1 << 0
MODE_ALPHA_NUM = 1 << 1
MODE_8BIT_BYTE = 1 << 2
MODE_KANJI = 1 << 3
# Encoding mode sizes.
MODE_SIZE_SMALL = {
MODE_NUMBER: 10,
MODE_ALPHA_NUM: 9,
MODE_8BIT_BYTE: 8,
MODE_KANJI: 8,
}
MODE_SIZE_MEDIUM = {
MODE_NUMBER: 12,
MODE_ALPHA_NUM: 11,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 10,
}
MODE_SIZE_LARGE = {
MODE_NUMBER: 14,
MODE_ALPHA_NUM: 13,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 12,
}
ALPHA_NUM = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
RE_ALPHA_NUM = re.compile(b"^[" + re.escape(ALPHA_NUM) + rb"]*\Z")
# The number of bits for numeric delimited data lengths.
NUMBER_LENGTH = {3: 10, 2: 7, 1: 4}
PATTERN_POSITION_TABLE = [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170],
]
G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
G18 = (
(1 << 12)
| (1 << 11)
| (1 << 10)
| (1 << 9)
| (1 << 8)
| (1 << 5)
| (1 << 2)
| (1 << 0)
)
G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
PAD0 = 0xEC
PAD1 = 0x11
# Precompute bit count limits, indexed by error correction level and code size
def _data_count(block):
return block.data_count
BIT_LIMIT_TABLE = [
[0]
+ [
8 * sum(map(_data_count, base.rs_blocks(version, error_correction)))
for version in range(1, 41)
]
for error_correction in range(4)
]
def BCH_type_info(data):
d = data << 10
while BCH_digit(d) - BCH_digit(G15) >= 0:
d ^= G15 << (BCH_digit(d) - BCH_digit(G15))
return ((data << 10) | d) ^ G15_MASK
def BCH_type_number(data):
d = data << 12
while BCH_digit(d) - BCH_digit(G18) >= 0:
d ^= G18 << (BCH_digit(d) - BCH_digit(G18))
return (data << 12) | d
def BCH_digit(data):
digit = 0
while data != 0:
digit += 1
data >>= 1
return digit
def pattern_position(version):
return PATTERN_POSITION_TABLE[version - 1]
def mask_func(pattern):
"""
Return the mask function for the given mask pattern.
"""
if pattern == 0: # 000
return lambda i, j: (i + j) % 2 == 0
if pattern == 1: # 001
return lambda i, j: i % 2 == 0
if pattern == 2: # 010
return lambda i, j: j % 3 == 0
if pattern == 3: # 011
return lambda i, j: (i + j) % 3 == 0
if pattern == 4: # 100
return lambda i, j: (math.floor(i / 2) + math.floor(j / 3)) % 2 == 0
if pattern == 5: # 101
return lambda i, j: (i * j) % 2 + (i * j) % 3 == 0
if pattern == 6: # 110
return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0
if pattern == 7: # 111
return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0
raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover
def mode_sizes_for_version(version):
if version < 10:
return MODE_SIZE_SMALL
elif version < 27:
return MODE_SIZE_MEDIUM
else:
return MODE_SIZE_LARGE
def length_in_bits(mode, version):
if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI):
raise TypeError(f"Invalid mode ({mode})") # pragma: no cover
check_version(version)
return mode_sizes_for_version(version)[mode]
def check_version(version):
if version < 1 or version > 40:
raise ValueError(f"Invalid version (was {version}, expected 1 to 40)")
def lost_point(modules):
modules_count = len(modules)
lost_point = 0
lost_point = _lost_point_level1(modules, modules_count)
lost_point += _lost_point_level2(modules, modules_count)
lost_point += _lost_point_level3(modules, modules_count)
lost_point += _lost_point_level4(modules, modules_count)
return lost_point
def _lost_point_level1(modules, modules_count):
lost_point = 0
modules_range = range(modules_count)
container = [0] * (modules_count + 1)
for row in modules_range:
this_row = modules[row]
previous_color = this_row[0]
length = 0
for col in modules_range:
if this_row[col] == previous_color:
length += 1
else:
if length >= 5:
container[length] += 1
length = 1
previous_color = this_row[col]
if length >= 5:
container[length] += 1
for col in modules_range:
previous_color = modules[0][col]
length = 0
for row in modules_range:
if modules[row][col] == previous_color:
length += 1
else:
if length >= 5:
container[length] += 1
length = 1
previous_color = modules[row][col]
if length >= 5:
container[length] += 1
lost_point += sum(
container[each_length] * (each_length - 2)
for each_length in range(5, modules_count + 1)
)
return lost_point
def _lost_point_level2(modules, modules_count):
lost_point = 0
modules_range = range(modules_count - 1)
for row in modules_range:
this_row = modules[row]
next_row = modules[row + 1]
# use iter() and next() to skip next four-block. e.g.
# d a f if top-right a != b bottom-right,
# c b e then both abcd and abef won't lost any point.
modules_range_iter = iter(modules_range)
for col in modules_range_iter:
top_right = this_row[col + 1]
if top_right != next_row[col + 1]:
# reduce 33.3% of runtime via next().
# None: raise nothing if there is no next item.
next(modules_range_iter, None)
elif top_right != this_row[col]:
continue
elif top_right != next_row[col]:
continue
else:
lost_point += 3
return lost_point
def _lost_point_level3(modules, modules_count):
# 1 : 1 : 3 : 1 : 1 ratio (dark:light:dark:light:dark) pattern in
# row/column, preceded or followed by light area 4 modules wide. From ISOIEC.
# pattern1: 10111010000
# pattern2: 00001011101
modules_range = range(modules_count)
modules_range_short = range(modules_count - 10)
lost_point = 0
for row in modules_range:
this_row = modules[row]
modules_range_short_iter = iter(modules_range_short)
col = 0
for col in modules_range_short_iter:
if (
not this_row[col + 1]
and this_row[col + 4]
and not this_row[col + 5]
and this_row[col + 6]
and not this_row[col + 9]
and (
this_row[col + 0]
and this_row[col + 2]
and this_row[col + 3]
and not this_row[col + 7]
and not this_row[col + 8]
and not this_row[col + 10]
or not this_row[col + 0]
and not this_row[col + 2]
and not this_row[col + 3]
and this_row[col + 7]
and this_row[col + 8]
and this_row[col + 10]
)
):
lost_point += 40
# horspool algorithm.
# if this_row[col + 10]:
# pattern1 shift 4, pattern2 shift 2. So min=2.
# else:
# pattern1 shift 1, pattern2 shift 1. So min=1.
if this_row[col + 10]:
next(modules_range_short_iter, None)
for col in modules_range:
modules_range_short_iter = iter(modules_range_short)
row = 0
for row in modules_range_short_iter:
if (
not modules[row + 1][col]
and modules[row + 4][col]
and not modules[row + 5][col]
and modules[row + 6][col]
and not modules[row + 9][col]
and (
modules[row + 0][col]
and modules[row + 2][col]
and modules[row + 3][col]
and not modules[row + 7][col]
and not modules[row + 8][col]
and not modules[row + 10][col]
or not modules[row + 0][col]
and not modules[row + 2][col]
and not modules[row + 3][col]
and modules[row + 7][col]
and modules[row + 8][col]
and modules[row + 10][col]
)
):
lost_point += 40
if modules[row + 10][col]:
next(modules_range_short_iter, None)
return lost_point
def _lost_point_level4(modules, modules_count):
dark_count = sum(map(sum, modules))
percent = float(dark_count) / (modules_count**2)
# Every 5% departure from 50%, rating++
rating = int(abs(percent * 100 - 50) / 5)
return rating * 10
def optimal_data_chunks(data, minimum=4):
"""
An iterator returning QRData chunks optimized to the data content.
:param minimum: The minimum number of bytes in a row to split as a chunk.
"""
data = to_bytestring(data)
num_pattern = rb"\d"
alpha_pattern = b"[" + re.escape(ALPHA_NUM) + b"]"
if len(data) <= minimum:
num_pattern = re.compile(b"^" + num_pattern + b"+$")
alpha_pattern = re.compile(b"^" + alpha_pattern + b"+$")
else:
re_repeat = b"{" + str(minimum).encode("ascii") + b",}"
num_pattern = re.compile(num_pattern + re_repeat)
alpha_pattern = re.compile(alpha_pattern + re_repeat)
num_bits = _optimal_split(data, num_pattern)
for is_num, chunk in num_bits:
if is_num:
yield QRData(chunk, mode=MODE_NUMBER, check_data=False)
else:
for is_alpha, sub_chunk in _optimal_split(chunk, alpha_pattern):
mode = MODE_ALPHA_NUM if is_alpha else MODE_8BIT_BYTE
yield QRData(sub_chunk, mode=mode, check_data=False)
def _optimal_split(data, pattern):
while data:
match = re.search(pattern, data)
if not match:
break
start, end = match.start(), match.end()
if start:
yield False, data[:start]
yield True, data[start:end]
data = data[end:]
if data:
yield False, data
def to_bytestring(data):
"""
Convert data to a (utf-8 encoded) byte-string if it isn't a byte-string
already.
"""
if not isinstance(data, bytes):
data = str(data).encode("utf-8")
return data
def optimal_mode(data):
"""
Calculate the optimal mode for this chunk of data.
"""
if data.isdigit():
return MODE_NUMBER
if RE_ALPHA_NUM.match(data):
return MODE_ALPHA_NUM
return MODE_8BIT_BYTE
class QRData:
"""
Data held in a QR compatible format.
Doesn't currently handle KANJI.
"""
def __init__(self, data, mode=None, check_data=True):
"""
If ``mode`` isn't provided, the most compact QR data type possible is
chosen.
"""
if check_data:
data = to_bytestring(data)
if mode is None:
self.mode = optimal_mode(data)
else:
self.mode = mode
if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE):
raise TypeError(f"Invalid mode ({mode})") # pragma: no cover
if check_data and mode < optimal_mode(data): # pragma: no cover
raise ValueError(f"Provided data can not be represented in mode {mode}")
self.data = data
def __len__(self):
return len(self.data)
def write(self, buffer):
if self.mode == MODE_NUMBER:
for i in range(0, len(self.data), 3):
chars = self.data[i : i + 3]
bit_length = NUMBER_LENGTH[len(chars)]
buffer.put(int(chars), bit_length)
elif self.mode == MODE_ALPHA_NUM:
for i in range(0, len(self.data), 2):
chars = self.data[i : i + 2]
if len(chars) > 1:
buffer.put(
ALPHA_NUM.find(chars[0]) * 45 + ALPHA_NUM.find(chars[1]), 11
)
else:
buffer.put(ALPHA_NUM.find(chars), 6)
else:
# Iterating a bytestring in Python 3 returns an integer,
# no need to ord().
data = self.data
for c in data:
buffer.put(c, 8)
def __repr__(self):
return repr(self.data)
class BitBuffer:
def __init__(self):
self.buffer: list[int] = []
self.length = 0
def __repr__(self):
return ".".join([str(n) for n in self.buffer])
def get(self, index):
buf_index = math.floor(index / 8)
return ((self.buffer[buf_index] >> (7 - index % 8)) & 1) == 1
def put(self, num, length):
for i in range(length):
self.put_bit(((num >> (length - i - 1)) & 1) == 1)
def __len__(self):
return self.length
def put_bit(self, bit):
buf_index = self.length // 8
if len(self.buffer) <= buf_index:
self.buffer.append(0)
if bit:
self.buffer[buf_index] |= 0x80 >> (self.length % 8)
self.length += 1
def create_bytes(buffer: BitBuffer, rs_blocks: list[RSBlock]):
offset = 0
maxDcCount = 0
maxEcCount = 0
dcdata: list[list[int]] = []
ecdata: list[list[int]] = []
for rs_block in rs_blocks:
dcCount = rs_block.data_count
ecCount = rs_block.total_count - dcCount
maxDcCount = max(maxDcCount, dcCount)
maxEcCount = max(maxEcCount, ecCount)
current_dc = [0xFF & buffer.buffer[i + offset] for i in range(dcCount)]
offset += dcCount
# Get error correction polynomial.
if ecCount in LUT.rsPoly_LUT:
rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0)
else:
rsPoly = base.Polynomial([1], 0)
for i in range(ecCount):
rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0)
rawPoly = base.Polynomial(current_dc, len(rsPoly) - 1)
modPoly = rawPoly % rsPoly
current_ec = []
mod_offset = len(modPoly) - ecCount
for i in range(ecCount):
modIndex = i + mod_offset
current_ec.append(modPoly[modIndex] if (modIndex >= 0) else 0)
dcdata.append(current_dc)
ecdata.append(current_ec)
data = []
for i in range(maxDcCount):
for dc in dcdata:
if i < len(dc):
data.append(dc[i])
for i in range(maxEcCount):
for ec in ecdata:
if i < len(ec):
data.append(ec[i])
return data
def create_data(version, error_correction, data_list):
buffer = BitBuffer()
for data in data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), length_in_bits(data.mode, version))
data.write(buffer)
# Calculate the maximum number of bits for the given version.
rs_blocks = base.rs_blocks(version, error_correction)
bit_limit = sum(block.data_count * 8 for block in rs_blocks)
if len(buffer) > bit_limit:
raise exceptions.DataOverflowError(
"Code length overflow. Data size (%s) > size available (%s)"
% (len(buffer), bit_limit)
)
# Terminate the bits (add up to four 0s).
for _ in range(min(bit_limit - len(buffer), 4)):
buffer.put_bit(False)
# Delimit the string into 8-bit words, padding with 0s if necessary.
delimit = len(buffer) % 8
if delimit:
for _ in range(8 - delimit):
buffer.put_bit(False)
# Add special alternating padding bitstrings until buffer is full.
bytes_to_fill = (bit_limit - len(buffer)) // 8
for i in range(bytes_to_fill):
if i % 2 == 0:
buffer.put(PAD0, 8)
else:
buffer.put(PAD1, 8)
return create_bytes(buffer, rs_blocks)