Skip to content

Commit 4a6ac34

Browse files
committed
Merge branch 'main' into output-reorder
2 parents f11fcdf + 4395902 commit 4a6ac34

File tree

14 files changed

+301
-77
lines changed

14 files changed

+301
-77
lines changed

CHANGES.rst

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,40 @@
22
Change log
33
==========
44

5-
8.1 ( 02 April 2025)
5+
WIP
6+
===
7+
8+
- Added ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_)
9+
- Improved test coveraged (akx in `#315`_)
10+
- Fixed typos in code that used ``embeded`` instead of ``embedded``. For backwards compatibility, the misspelled parameter names are still accepted but now emit deprecation warnings. These deprecated parameter names will be removed in v9.0. (benjnicholls in `#349`_)
11+
- Migrate pyproject.toml to PEP 621-compliant [project] metadata format. (hroncok in `#399`_)
12+
- Allow execution as a Python module. (stefansjs in `#400`_)
13+
14+
::
15+
16+
python -m qrcode --output qrcode.png "hello world"
17+
18+
.. _#315: https://github.com/lincolnloop/python-qrcode/pull/315
19+
.. _#349: https://github.com/lincolnloop/python-qrcode/pull/349
20+
.. _#373: https://github.com/lincolnloop/python-qrcode/pull/373
21+
.. _#399: https://github.com/lincolnloop/python-qrcode/pull/399
22+
.. _#400: https://github.com/lincolnloop/python-qrcode/pull/400
23+
24+
8.2 (01 May 2025)
25+
=================
26+
27+
- Optimize QRColorMask apply_mask method for enhanced performance
28+
- Fix typos on StyledPilImage embeded_* parameters.
29+
The old parameters with the typos are still accepted
30+
for backward compatibility.
31+
32+
33+
8.1 (02 April 2025)
634
====================
735

836
- Added support for Python 3.13.
937

10-
8.0 ( 27 September 2024)
38+
8.0 (27 September 2024)
1139
========================
1240

1341
- Added support for Python 3.11 and 3.12.
@@ -20,10 +48,12 @@ Change log
2048

2149
- Code quality and formatting utilises ruff_.
2250

23-
- Removed ``typing_extensions`` as a dependency, as it's no longer required with
51+
- Removed ``typing_extensions`` as a dependency, as it's no longer required
52+
with having Python 3.9+ as a requirement.
2453
having Python 3.9+ as a requirement.
2554

26-
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) when generating
55+
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`)
56+
when generating
2757
QR codes with embedded images to ensure content is readable
2858

2959
.. _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: 47 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.1"
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",
@@ -28,6 +28,31 @@ classifiers = [
2828
"Topic :: Multimedia :: Graphics",
2929
"Topic :: Software Development :: Libraries :: Python Modules",
3030
]
31+
requires-python = "~=3.9"
32+
dependencies = [
33+
"colorama; sys_platform == 'win32'",
34+
"deprecation",
35+
]
36+
37+
38+
[project.optional-dependencies]
39+
pil = ["pillow >=9.1.0"]
40+
png = ["pypng"]
41+
all = ["pypng", "pillow >=9.1.0"]
42+
43+
[project.urls]
44+
homepage = "https://github.com/lincolnloop/python-qrcode"
45+
repository = "https://github.com/lincolnloop/python-qrcode.git"
46+
documentation = "https://github.com/lincolnloop/python-qrcode#readme"
47+
changelog = "https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst"
48+
"Bug Tracker" = "https://github.com/lincolnloop/python-qrcode/issues"
49+
50+
[project.scripts]
51+
qr = "qrcode.console_scripts:main"
52+
53+
[tool.poetry]
54+
packages = [{include = "qrcode"}]
55+
readme = ["README.rst", "CHANGES.rst"]
3156

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

39-
[tool.poetry.scripts]
40-
qr = 'qrcode.console_scripts:main'
41-
42-
43-
[tool.poetry.dependencies]
44-
python = "^3.9"
45-
colorama = {version = "*", platform = "win32"}
46-
pypng = {version = "*", optional = true}
47-
pillow = {version = ">=9.1.0", optional = true}
48-
49-
[tool.poetry.extras]
50-
pil = ["pillow"]
51-
png = ["pypng"]
52-
all = ["pypng","pillow"]
53-
5464
[tool.poetry.group.dev.dependencies]
5565
pytest = {version = "*"}
5666
pytest-cov = {version = "*"}
@@ -71,3 +81,17 @@ date-format =" %%-d %%B %%Y"
7181
prereleaser.middle = [
7282
"qrcode.release.update_manpage"
7383
]
84+
85+
[tool.coverage.run]
86+
source = ["qrcode"]
87+
parallel = true
88+
89+
[tool.coverage.report]
90+
exclude_lines = [
91+
"@abc.abstractmethod",
92+
"@overload",
93+
"if (typing\\.)?TYPE_CHECKING:",
94+
"pragma: no cover",
95+
"raise NotImplementedError"
96+
]
97+
skip_covered = true

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/image/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]]
1111

1212

13-
class BaseImage:
13+
class BaseImage(abc.ABC):
1414
"""
1515
Base QRCode image output class.
1616
"""

qrcode/image/styledpil.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import qrcode.image.base
1+
from __future__ import annotations
2+
3+
import warnings
4+
from typing import overload
5+
6+
import deprecation
27
from PIL import Image
8+
9+
import qrcode.image.base
310
from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask
411
from qrcode.image.styles.moduledrawers import SquareModuleDrawer
512

@@ -29,7 +36,7 @@ class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
2936
data integrity A resampling filter can be specified (defaulting to
3037
PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible
3138
options for this parameter.
32-
The image size can be controlled by `embeded_image_ratio` which is a ratio
39+
The image size can be controlled by `embedded_image_ratio` which is a ratio
3340
between 0 and 1 that's set in relation to the overall width of the QR code.
3441
"""
3542

@@ -41,14 +48,32 @@ class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
4148

4249
def __init__(self, *args, **kwargs):
4350
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
51+
52+
if kwargs.get("embeded_image_path") or kwargs.get("embeded_image"):
53+
warnings.warn(
54+
"The 'embeded_*' parameters are deprecated. Use 'embedded_image_path' "
55+
"or 'embedded_image' instead. The 'embeded_*' parameters will be "
56+
"removed in v9.0.",
57+
category=DeprecationWarning,
58+
stacklevel=2,
59+
)
60+
61+
# allow embeded_ parameters with typos for backwards compatibility
62+
embedded_image_path = kwargs.get(
63+
"embedded_image_path", kwargs.get("embeded_image_path", None)
64+
)
65+
self.embedded_image = kwargs.get(
66+
"embedded_image", kwargs.get("embeded_image", None)
67+
)
68+
self.embedded_image_ratio = kwargs.get(
69+
"embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25)
70+
)
71+
self.embedded_image_resample = kwargs.get(
72+
"embedded_image_resample",
73+
kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS),
4974
)
50-
if not self.embeded_image and embeded_image_path:
51-
self.embeded_image = Image.open(embeded_image_path)
75+
if not self.embedded_image and embedded_image_path:
76+
self.embedded_image = Image.open(embedded_image_path)
5277

5378
# the paint_color is the color the module drawer will use to draw upon
5479
# a canvas During the color mask process, pixels that are paint_color
@@ -59,12 +84,18 @@ def __init__(self, *args, **kwargs):
5984

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

87+
@overload
88+
def drawrect(self, row, col):
89+
"""
90+
Not used.
91+
"""
92+
6293
def new_image(self, **kwargs):
6394
mode = (
6495
"RGBA"
6596
if (
6697
self.color_mask.has_transparency
67-
or (self.embeded_image and "A" in self.embeded_image.getbands())
98+
or (self.embedded_image and "A" in self.embedded_image.getbands())
6899
)
69100
else "RGB"
70101
)
@@ -79,23 +110,32 @@ def init_new_image(self):
79110

80111
def process(self):
81112
self.color_mask.apply_mask(self._img)
82-
if self.embeded_image:
83-
self.draw_embeded_image()
84-
113+
if self.embedded_image:
114+
self.draw_embedded_image()
115+
116+
@deprecation.deprecated(
117+
deprecated_in="9.0",
118+
removed_in="8.3",
119+
current_version="8.2",
120+
details="Use draw_embedded_image() instead",
121+
)
85122
def draw_embeded_image(self):
86-
if not self.embeded_image:
123+
return self.draw_embedded_image()
124+
125+
def draw_embedded_image(self):
126+
if not self.embedded_image:
87127
return
88128
total_width, _ = self._img.size
89129
total_width = int(total_width)
90-
logo_width_ish = int(total_width * self.embeded_image_ratio)
130+
logo_width_ish = int(total_width * self.embedded_image_ratio)
91131
logo_offset = (
92132
int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size)
93133
* self.box_size
94134
) # round the offset to the nearest module
95135
logo_position = (logo_offset, logo_offset)
96136
logo_width = total_width - logo_offset * 2
97-
region = self.embeded_image
98-
region = region.resize((logo_width, logo_width), self.embeded_image_resample)
137+
region = self.embedded_image
138+
region = region.resize((logo_width, logo_width), self.embedded_image_resample)
99139
if "A" in region.getbands():
100140
self._img.alpha_composite(region, logo_position)
101141
else:
@@ -104,8 +144,7 @@ def draw_embeded_image(self):
104144
def save(self, stream, format=None, **kwargs):
105145
if format is None:
106146
format = kwargs.get("kind", self.kind)
107-
if "kind" in kwargs:
108-
del kwargs["kind"]
147+
kwargs.pop("kind", None)
109148
self._img.save(stream, format=format, **kwargs)
110149

111150
def __getattr__(self, name):

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
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# For backwards compatibility, importing the PIL drawers here.
22
try:
33
from .pil import CircleModuleDrawer # noqa: F401
4+
from .pil import GappedCircleModuleDrawer # noqa: F401
45
from .pil import GappedSquareModuleDrawer # noqa: F401
56
from .pil import HorizontalBarsDrawer # noqa: F401
67
from .pil import RoundedModuleDrawer # noqa: F401
78
from .pil import SquareModuleDrawer # noqa: F401
89
from .pil import VerticalBarsDrawer # noqa: F401
9-
except ImportError:
10+
except ImportError: # pragma: no cover
1011
pass

0 commit comments

Comments
 (0)