Skip to content

Commit b1d4155

Browse files
committed
Merge branch 'main' into feature/svg-rounded-module-drawer
2 parents 3d8df95 + 9c6d254 commit b1d4155

File tree

16 files changed

+224
-100
lines changed

16 files changed

+224
-100
lines changed

.github/workflows/push.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
max-parallel: 4
1010
matrix:
11-
python-version: ["3.9", "3.10", "3.11", "3.12"]
11+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1212

1313
steps:
1414
- uses: actions/checkout@v4

CHANGES.rst

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,35 @@
22
Change log
33
==========
44

5-
8.0 ( 27 September 2024)
5+
WIP
6+
===
7+
8+
- Migrate pyproject.toml to PEP 621-compliant [project] metadata format.
9+
(hroncok in `#399`_)
10+
- Allow execution as a Python module. (stefansjs in `#400`_)
11+
12+
::
13+
14+
python -m qrcode --output qrcode.png "hello world"
15+
16+
.. _#399: https://github.com/lincolnloop/python-qrcode/pull/399
17+
.. _#400: https://github.com/lincolnloop/python-qrcode/pull/400
18+
19+
8.2 (01 May 2025)
20+
=================
21+
22+
- Optimize QRColorMask apply_mask method for enhanced performance
23+
- Fix typos on StyledPilImage embeded_* parameters.
24+
The old parameters with the typos are still accepted
25+
for backward compatibility.
26+
27+
28+
8.1 (02 April 2025)
29+
====================
30+
31+
- Added support for Python 3.13.
32+
33+
8.0 (27 September 2024)
634
========================
735

836
- Added support for Python 3.11 and 3.12.
@@ -15,10 +43,12 @@ Change log
1543

1644
- Code quality and formatting utilises ruff_.
1745

18-
- Removed ``typing_extensions`` as a dependency, as it's no longer required with
46+
- Removed ``typing_extensions`` as a dependency, as it's no longer required
47+
with having Python 3.9+ as a requirement.
1948
having Python 3.9+ as a requirement.
2049

21-
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) when generating
50+
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`)
51+
when generating
2252
QR codes with embedded images to ensure content is readable
2353

2454
.. _Poetry: https://python-poetry.org

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ circles by reducing this less than the default of ``Decimal(1)``.
211211

212212
The ``StyledPilImage`` additionally accepts an optional ``color_mask``
213213
parameter to change the colors of the QR Code, and an optional
214-
``embeded_image_path`` to embed an image in the center of the code.
214+
``embedded_image_path`` to embed an image in the center of the code.
215215

216216
Other color masks:
217217

@@ -232,7 +232,7 @@ and an embedded image:
232232
233233
img_1 = qr.make_image(image_factory=StyledPilImage, module_drawer=RoundedModuleDrawer())
234234
img_2 = qr.make_image(image_factory=StyledPilImage, color_mask=RadialGradiantColorMask())
235-
img_3 = qr.make_image(image_factory=StyledPilImage, embeded_image_path="/path/to/image.png")
235+
img_3 = qr.make_image(image_factory=StyledPilImage, embedded_image_path="/path/to/image.png")
236236
237237
Examples
238238
========

pyproject.toml

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
[build-system]
2-
requires = ["poetry-core"]
2+
requires = ["poetry-core>=2"]
33
build-backend = "poetry.core.masonry.api"
44

5-
[tool.poetry]
5+
[project]
66
name = "qrcode"
7-
version = "8.0"
8-
packages = [{include = "qrcode"}]
7+
version = "8.2"
98
description = "QR Code image generator"
10-
authors = ["Lincoln Loop <[email protected]>"]
11-
license = "BSD"
12-
readme = ["README.rst", "CHANGES.rst"]
13-
homepage = "https://github.com/lincolnloop/python-qrcode"
9+
authors = [
10+
{ name = "Lincoln Loop", email = "[email protected]" },
11+
]
12+
license = { text = "BSD-3-Clause" }
13+
dynamic = [ "readme" ]
1414
keywords = ["qr", "denso-wave", "IEC18004"]
1515
classifiers = [
1616
"Development Status :: 5 - Production/Stable",
@@ -23,10 +23,35 @@ classifiers = [
2323
"Programming Language :: Python :: 3.10",
2424
"Programming Language :: Python :: 3.11",
2525
"Programming Language :: Python :: 3.12",
26+
"Programming Language :: Python :: 3.13",
2627
"Programming Language :: Python :: 3 :: Only",
2728
"Topic :: Multimedia :: Graphics",
2829
"Topic :: Software Development :: Libraries :: Python Modules",
2930
]
31+
requires-python = "~=3.9"
32+
dependencies = [
33+
"colorama; sys_platform == 'win32'",
34+
]
35+
36+
37+
[project.optional-dependencies]
38+
pil = ["pillow >=9.1.0"]
39+
png = ["pypng"]
40+
all = ["pypng", "pillow >=9.1.0"]
41+
42+
[project.urls]
43+
homepage = "https://github.com/lincolnloop/python-qrcode"
44+
repository = "https://github.com/lincolnloop/python-qrcode.git"
45+
documentation = "https://github.com/lincolnloop/python-qrcode#readme"
46+
changelog = "https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst"
47+
"Bug Tracker" = "https://github.com/lincolnloop/python-qrcode/issues"
48+
49+
[project.scripts]
50+
qr = "qrcode.console_scripts:main"
51+
52+
[tool.poetry]
53+
packages = [{include = "qrcode"}]
54+
readme = ["README.rst", "CHANGES.rst"]
3055

3156
# There is no support for data files yet.
3257
# https://github.com/python-poetry/poetry/issues/9519
@@ -35,21 +60,6 @@ classifiers = [
3560
# { destination = "share/man/man1", from = [ "doc/qr.1" ] },
3661
# ]
3762

38-
[tool.poetry.scripts]
39-
qr = 'qrcode.console_scripts:main'
40-
41-
42-
[tool.poetry.dependencies]
43-
python = "^3.9"
44-
colorama = {version = "*", platform = "win32"}
45-
pypng = {version = "*", optional = true}
46-
pillow = {version = ">=9.1.0", optional = true}
47-
48-
[tool.poetry.extras]
49-
pil = ["pillow"]
50-
png = ["pypng"]
51-
all = ["pypng","pillow"]
52-
5363
[tool.poetry.group.dev.dependencies]
5464
pytest = {version = "*"}
5565
pytest-cov = {version = "*"}

qrcode/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .console_scripts import main
2+
3+
main()

qrcode/console_scripts.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import optparse
1010
import os
1111
import sys
12-
from typing import Dict, Iterable, NoReturn, Optional, Set, Type
12+
from typing import NoReturn, Optional
13+
from collections.abc import Iterable
1314
from importlib import metadata
1415

1516
import qrcode
@@ -140,7 +141,7 @@ def raise_error(msg: str) -> NoReturn:
140141
img.save(sys.stdout.buffer)
141142

142143

143-
def get_factory(module: str) -> Type[BaseImage]:
144+
def get_factory(module: str) -> type[BaseImage]:
144145
if "." not in module:
145146
raise ValueError("The image factory is not a full python path")
146147
module, name = module.rsplit(".", 1)
@@ -149,7 +150,7 @@ def get_factory(module: str) -> Type[BaseImage]:
149150

150151

151152
def get_drawer_help() -> str:
152-
help: Dict[str, Set] = {}
153+
help: dict[str, set] = {}
153154
for alias, module in default_factories.items():
154155
try:
155156
image = get_factory(module)

qrcode/image/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import abc
2-
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union
2+
from typing import TYPE_CHECKING, Any, Optional, Union
33

44
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
55

66
if TYPE_CHECKING:
77
from qrcode.main import ActiveWithNeighbors, QRCode
88

99

10-
DrawerAliases = Dict[str, Tuple[Type[QRModuleDrawer], Dict[str, Any]]]
10+
DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]]
1111

1212

1313
class BaseImage:
@@ -16,7 +16,7 @@ class BaseImage:
1616
"""
1717

1818
kind: Optional[str] = None
19-
allowed_kinds: Optional[Tuple[str]] = None
19+
allowed_kinds: Optional[tuple[str]] = None
2020
needs_context = False
2121
needs_processing = False
2222
needs_drawrect = True
@@ -108,7 +108,7 @@ def is_eye(self, row: int, col: int):
108108

109109

110110
class BaseImageWithDrawer(BaseImage):
111-
default_drawer_class: Type[QRModuleDrawer]
111+
default_drawer_class: type[QRModuleDrawer]
112112
drawer_aliases: DrawerAliases = {}
113113

114114
def get_default_module_drawer(self) -> QRModuleDrawer:

qrcode/image/styledpil.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
2929
data integrity A resampling filter can be specified (defaulting to
3030
PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible
3131
options for this parameter.
32-
The image size can be controlled by `embeded_image_ratio` which is a ratio
32+
The image size can be controlled by `embedded_image_ratio` which is a ratio
3333
between 0 and 1 that's set in relation to the overall width of the QR code.
3434
"""
3535

@@ -41,14 +41,22 @@ class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
4141

4242
def __init__(self, *args, **kwargs):
4343
self.color_mask = kwargs.get("color_mask", SolidFillColorMask())
44-
embeded_image_path = kwargs.get("embeded_image_path", None)
45-
self.embeded_image = kwargs.get("embeded_image", None)
46-
self.embeded_image_ratio = kwargs.get("embeded_image_ratio", 0.25)
47-
self.embeded_image_resample = kwargs.get(
48-
"embeded_image_resample", Image.Resampling.LANCZOS
44+
# allow embeded_ parameters with typos for backwards compatibility
45+
embedded_image_path = kwargs.get(
46+
"embedded_image_path", kwargs.get("embeded_image_path", None)
4947
)
50-
if not self.embeded_image and embeded_image_path:
51-
self.embeded_image = Image.open(embeded_image_path)
48+
self.embedded_image = kwargs.get(
49+
"embedded_image", kwargs.get("embeded_image", None)
50+
)
51+
self.embedded_image_ratio = kwargs.get(
52+
"embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25)
53+
)
54+
self.embedded_image_resample = kwargs.get(
55+
"embedded_image_resample",
56+
kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS),
57+
)
58+
if not self.embedded_image and embedded_image_path:
59+
self.embedded_image = Image.open(embedded_image_path)
5260

5361
# the paint_color is the color the module drawer will use to draw upon
5462
# a canvas During the color mask process, pixels that are paint_color
@@ -64,7 +72,7 @@ def new_image(self, **kwargs):
6472
"RGBA"
6573
if (
6674
self.color_mask.has_transparency
67-
or (self.embeded_image and "A" in self.embeded_image.getbands())
75+
or (self.embedded_image and "A" in self.embedded_image.getbands())
6876
)
6977
else "RGB"
7078
)
@@ -79,23 +87,23 @@ def init_new_image(self):
7987

8088
def process(self):
8189
self.color_mask.apply_mask(self._img)
82-
if self.embeded_image:
83-
self.draw_embeded_image()
90+
if self.embedded_image:
91+
self.draw_embedded_image()
8492

85-
def draw_embeded_image(self):
86-
if not self.embeded_image:
93+
def draw_embedded_image(self):
94+
if not self.embedded_image:
8795
return
8896
total_width, _ = self._img.size
8997
total_width = int(total_width)
90-
logo_width_ish = int(total_width * self.embeded_image_ratio)
98+
logo_width_ish = int(total_width * self.embedded_image_ratio)
9199
logo_offset = (
92100
int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size)
93101
* self.box_size
94102
) # round the offset to the nearest module
95103
logo_position = (logo_offset, logo_offset)
96104
logo_width = total_width - logo_offset * 2
97-
region = self.embeded_image
98-
region = region.resize((logo_width, logo_width), self.embeded_image_resample)
105+
region = self.embedded_image
106+
region = region.resize((logo_width, logo_width), self.embedded_image_resample)
99107
if "A" in region.getbands():
100108
self._img.alpha_composite(region, logo_position)
101109
else:

qrcode/image/styles/colormasks.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,33 @@ class QRColorMask:
2727
def initialize(self, styledPilImage, image):
2828
self.paint_color = styledPilImage.paint_color
2929

30-
def apply_mask(self, image):
30+
def apply_mask(self, image, use_cache=False):
3131
width, height = image.size
32+
pixels = image.load()
33+
fg_color_cache = {} if use_cache else None
3234
for x in range(width):
3335
for y in range(height):
36+
current_color = pixels[x, y]
37+
if current_color == self.back_color:
38+
continue
39+
if use_cache and current_color in fg_color_cache:
40+
pixels[x, y] = fg_color_cache[current_color]
41+
continue
3442
norm = self.extrap_color(
35-
self.back_color, self.paint_color, image.getpixel((x, y))
43+
self.back_color, self.paint_color, current_color
3644
)
3745
if norm is not None:
38-
image.putpixel(
39-
(x, y),
40-
self.interp_color(
41-
self.get_bg_pixel(image, x, y),
42-
self.get_fg_pixel(image, x, y),
43-
norm,
44-
),
46+
new_color = self.interp_color(
47+
self.get_bg_pixel(image, x, y),
48+
self.get_fg_pixel(image, x, y),
49+
norm,
4550
)
51+
pixels[x, y] = new_color
52+
53+
if use_cache:
54+
fg_color_cache[current_color] = new_color
4655
else:
47-
image.putpixel((x, y), self.get_bg_pixel(image, x, y))
56+
pixels[x, y] = self.get_bg_pixel(image, x, y)
4857

4958
def get_fg_pixel(self, image, x, y):
5059
raise NotImplementedError("QRModuleDrawer.paint_fg_pixel")
@@ -103,7 +112,7 @@ def apply_mask(self, image):
103112
# the individual pixel comparisons that the base class uses, which
104113
# would be a lot faster. (In fact doing this would probably remove
105114
# the need for the B&W optimization above.)
106-
QRColorMask.apply_mask(self, image)
115+
QRColorMask.apply_mask(self, image, use_cache=True)
107116

108117
def get_fg_pixel(self, image, x, y):
109118
return self.front_color

qrcode/image/styles/moduledrawers/pil.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, List
1+
from typing import TYPE_CHECKING
22

33
from PIL import Image, ImageDraw
44
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
@@ -136,7 +136,7 @@ def setup_corners(self):
136136
self.SE_ROUND = self.NW_ROUND.transpose(Image.Transpose.ROTATE_180)
137137
self.NE_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
138138

139-
def drawrect(self, box: List[List[int]], is_active: "ActiveWithNeighbors"):
139+
def drawrect(self, box: list[list[int]], is_active: "ActiveWithNeighbors"):
140140
if not is_active:
141141
return
142142
# find rounded edges

0 commit comments

Comments
 (0)