Skip to content

Commit b0c91ae

Browse files
committed
Merge branch 'main' into svg-compressed
2 parents 2603686 + ac2dda2 commit b0c91ae

File tree

18 files changed

+335
-106
lines changed

18 files changed

+335
-106
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: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,42 @@
22
Change log
33
==========
44

5-
8.0 ( 27 September 2024)
5+
WIP
6+
===
7+
8+
- Added ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_)
9+
- Removed the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_)
10+
- Improved test coveraged (akx in `#315`_)
11+
- 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`_)
12+
- Migrate pyproject.toml to PEP 621-compliant [project] metadata format. (hroncok in `#399`_)
13+
- Allow execution as a Python module. (stefansjs in `#400`_)
14+
15+
::
16+
17+
python -m qrcode --output qrcode.png "hello world"
18+
19+
.. _#315: https://github.com/lincolnloop/python-qrcode/pull/315
20+
.. _#349: https://github.com/lincolnloop/python-qrcode/pull/349
21+
.. _#373: https://github.com/lincolnloop/python-qrcode/pull/373
22+
.. _#385: https://github.com/lincolnloop/python-qrcode/pull/385
23+
.. _#399: https://github.com/lincolnloop/python-qrcode/pull/399
24+
.. _#400: https://github.com/lincolnloop/python-qrcode/pull/400
25+
26+
8.2 (01 May 2025)
27+
=================
28+
29+
- Optimize QRColorMask apply_mask method for enhanced performance
30+
- Fix typos on StyledPilImage embeded_* parameters.
31+
The old parameters with the typos are still accepted
32+
for backward compatibility.
33+
34+
35+
8.1 (02 April 2025)
36+
====================
37+
38+
- Added support for Python 3.13.
39+
40+
8.0 (27 September 2024)
641
========================
742

843
- Added support for Python 3.11 and 3.12.
@@ -15,10 +50,12 @@ Change log
1550

1651
- Code quality and formatting utilises ruff_.
1752

18-
- Removed ``typing_extensions`` as a dependency, as it's no longer required with
53+
- Removed ``typing_extensions`` as a dependency, as it's no longer required
54+
with having Python 3.9+ as a requirement.
1955
having Python 3.9+ as a requirement.
2056

21-
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) when generating
57+
- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`)
58+
when generating
2259
QR codes with embedded images to ensure content is readable
2360

2461
.. _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: 48 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,36 @@ 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+
"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"]
3056

3157
# There is no support for data files yet.
3258
# https://github.com/python-poetry/poetry/issues/9519
@@ -35,21 +61,6 @@ classifiers = [
3561
# { destination = "share/man/man1", from = [ "doc/qr.1" ] },
3662
# ]
3763

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-
5364
[tool.poetry.group.dev.dependencies]
5465
pytest = {version = "*"}
5566
pytest-cov = {version = "*"}
@@ -70,3 +81,17 @@ date-format =" %%-d %%B %%Y"
7081
prereleaser.middle = [
7182
"qrcode.release.update_manpage"
7283
]
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/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
@@ -145,7 +146,7 @@ def raise_error(msg: str) -> NoReturn:
145146
img.save(sys.stdout.buffer)
146147

147148

148-
def get_factory(module: str) -> Type[BaseImage]:
149+
def get_factory(module: str) -> type[BaseImage]:
149150
if "." not in module:
150151
raise ValueError("The image factory is not a full python path")
151152
module, name = module.rsplit(".", 1)
@@ -154,7 +155,7 @@ def get_factory(module: str) -> Type[BaseImage]:
154155

155156

156157
def get_drawer_help() -> str:
157-
help: Dict[str, Set] = {}
158+
help: dict[str, set] = {}
158159
for alias, module in default_factories.items():
159160
try:
160161
image = get_factory(module)

qrcode/image/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
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

13-
class BaseImage:
13+
class BaseImage(abc.ABC):
1414
"""
1515
Base QRCode image output class.
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: 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):

0 commit comments

Comments
 (0)