From 3d8df956a072ba9e04c3cea45f284e617a66acf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Plo=CC=88tz?= Date: Wed, 12 Mar 2025 16:21:47 +0100 Subject: [PATCH 1/6] feat: add SvgRoundedModuleDrawer class for SVG QR codes with rounded corners --- qrcode/image/styles/moduledrawers/svg.py | 135 +++++++++++++++++++++++ qrcode/tests/test_qrcode_svg.py | 28 +++++ 2 files changed, 163 insertions(+) diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index cf5b9e7d..ea16174f 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -137,3 +137,138 @@ def subpath(self, box) -> str: # 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" + + +class SvgRoundedModuleDrawer(SvgPathQRModuleDrawer): + """ + 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: Decimal = Decimal(1), **kwargs): + super().__init__(**kwargs) + self.radius_ratio = radius_ratio + + def initialize(self, *args, **kwargs) -> None: + super().initialize(*args, **kwargs) + self.corner_radius = self.box_half * self.radius_ratio + + def drawrect(self, box, is_active): + if not is_active: + return + + # Check if is_active has neighbor information (ActiveWithNeighbors object) + if hasattr(is_active, 'N'): + # Neighbor information is available + self.img._subpaths.append(self.subpath(box, is_active)) + else: + # No neighbor information available, draw a square with all corners rounded + self.img._subpaths.append(self.subpath_all_rounded(box)) + + def subpath_all_rounded(self, box) -> str: + """Draw a module with all corners rounded.""" + 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) + r = self.img.units(self.corner_radius, text=False) + + # Build the path with all corners rounded + path = [] + + # Start at top-left after the rounded part + path.append(f"M{x0 + r},{y0}") + + # Top edge to top-right corner + path.append(f"H{x1 - r}") + # Top-right rounded corner + path.append(f"A{r},{r} 0 0 1 {x1},{y0 + r}") + + # Right edge to bottom-right corner + path.append(f"V{y1 - r}") + # Bottom-right rounded corner + path.append(f"A{r},{r} 0 0 1 {x1 - r},{y1}") + + # Bottom edge to bottom-left corner + path.append(f"H{x0 + r}") + # Bottom-left rounded corner + path.append(f"A{r},{r} 0 0 1 {x0},{y1 - r}") + + # Left edge to top-left corner + path.append(f"V{y0 + r}") + # Top-left rounded corner + path.append(f"A{r},{r} 0 0 1 {x0 + r},{y0}") + + # Close the path + path.append("z") + + return "".join(path) + + def subpath(self, box, is_active) -> str: + """Draw a module with corners rounded based on neighbor information.""" + # Determine which corners should be rounded + 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 + + 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) + r = self.img.units(self.corner_radius, text=False) + + # Build the path + path = [] + + # Start at top-left and move clockwise + if nw_rounded: + # Start at top-left corner, after the rounded part + path.append(f"M{x0 + r},{y0}") + else: + # Start at the top-left corner + path.append(f"M{x0},{y0}") + + # Top edge to top-right corner + if ne_rounded: + path.append(f"H{x1 - r}") + # Top-right rounded corner + path.append(f"A{r},{r} 0 0 1 {x1},{y0 + r}") + else: + path.append(f"H{x1}") + + # Right edge to bottom-right corner + if se_rounded: + path.append(f"V{y1 - r}") + # Bottom-right rounded corner + path.append(f"A{r},{r} 0 0 1 {x1 - r},{y1}") + else: + path.append(f"V{y1}") + + # Bottom edge to bottom-left corner + if sw_rounded: + path.append(f"H{x0 + r}") + # Bottom-left rounded corner + path.append(f"A{r},{r} 0 0 1 {x0},{y1 - r}") + else: + path.append(f"H{x0}") + + # Left edge back to start + if nw_rounded: + path.append(f"V{y0 + r}") + # Top-left rounded corner + path.append(f"A{r},{r} 0 0 1 {x0 + r},{y0}") + else: + path.append(f"V{y0}") + + # Close the path + path.append("z") + + return "".join(path) diff --git a/qrcode/tests/test_qrcode_svg.py b/qrcode/tests/test_qrcode_svg.py index 4774b245..20075593 100644 --- a/qrcode/tests/test_qrcode_svg.py +++ b/qrcode/tests/test_qrcode_svg.py @@ -2,6 +2,8 @@ import qrcode from qrcode.image import svg +from qrcode.image.styles.moduledrawers.svg import SvgRoundedModuleDrawer +from decimal import Decimal from qrcode.tests.consts import UNICODE_TEXT @@ -52,3 +54,29 @@ def test_svg_circle_drawer(): qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle") img.save(io.BytesIO()) + + +def test_svg_rounded_module_drawer(): + """Test that the SvgRoundedModuleDrawer works correctly.""" + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + + # Test with default parameters + module_drawer = SvgRoundedModuleDrawer() + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) + img.save(io.BytesIO()) + + # Test with custom radius_ratio + module_drawer = SvgRoundedModuleDrawer(radius_ratio=Decimal('0.5')) + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) + img.save(io.BytesIO()) + + # Test with custom size_ratio + module_drawer = SvgRoundedModuleDrawer(size_ratio=Decimal('0.8')) + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) + img.save(io.BytesIO()) + + # Test with both custom parameters + module_drawer = SvgRoundedModuleDrawer(radius_ratio=Decimal('0.3'), size_ratio=Decimal('0.9')) + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) + img.save(io.BytesIO()) From c026cd3af9dae80835fec7488fcc086999eb7f43 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 22 Jul 2025 14:29:17 +0200 Subject: [PATCH 2/6] Consistent Type annotation setup, overall typo fixes --- qrcode/LUT.py | 10 +++++----- qrcode/__init__.py | 2 ++ qrcode/base.py | 2 ++ qrcode/console_scripts.py | 8 +++++--- qrcode/constants.py | 10 ++++++---- qrcode/exceptions.py | 3 +++ qrcode/image/base.py | 10 ++++++---- qrcode/image/pil.py | 2 ++ qrcode/image/pure.py | 2 ++ qrcode/image/styledpil.py | 2 ++ qrcode/image/styles/colormasks.py | 2 ++ qrcode/image/styles/moduledrawers/base.py | 2 ++ qrcode/image/styles/moduledrawers/pil.py | 2 ++ qrcode/image/svg.py | 10 ++++++---- qrcode/main.py | 9 +++++---- qrcode/release.py | 2 ++ qrcode/util.py | 2 ++ 17 files changed, 56 insertions(+), 24 deletions(-) diff --git a/qrcode/LUT.py b/qrcode/LUT.py index 115892f1..e0ea906f 100644 --- a/qrcode/LUT.py +++ b/qrcode/LUT.py @@ -1,10 +1,10 @@ -# Store all kinds of lookup table. - - +""" +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 @@ -13,7 +13,7 @@ # 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): diff --git a/qrcode/__init__.py b/qrcode/__init__.py index 6b238d33..4bdbacc3 100644 --- a/qrcode/__init__.py +++ b/qrcode/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from qrcode.main import QRCode from qrcode.main import make # noqa from qrcode.constants import ( # noqa diff --git a/qrcode/base.py b/qrcode/base.py index 20f81f6f..3f806317 100644 --- a/qrcode/base.py +++ b/qrcode/base.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import NamedTuple from qrcode import constants diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index ebe8810f..ad69041e 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -6,10 +6,12 @@ a pipe to a file an image is written. The default image format is PNG. """ +from __future__ import annotations + import optparse import os import sys -from typing import NoReturn, Optional +from typing import NoReturn from collections.abc import Iterable from importlib import metadata @@ -122,7 +124,7 @@ def raise_error(msg: str) -> NoReturn: return kwargs = {} - aliases: Optional[DrawerAliases] = getattr( + aliases: DrawerAliases | None = getattr( qr.image_factory, "drawer_aliases", None ) if opts.factory_drawer: @@ -156,7 +158,7 @@ def get_drawer_help() -> str: image = get_factory(module) except ImportError: # pragma: no cover continue - aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None) + aliases: DrawerAliases | None = getattr(image, "drawer_aliases", None) if not aliases: continue factories = help.setdefault(commas(aliases), set()) diff --git a/qrcode/constants.py b/qrcode/constants.py index 385dda08..defdfab8 100644 --- a/qrcode/constants.py +++ b/qrcode/constants.py @@ -1,5 +1,7 @@ +from __future__ import annotations + # QR error correct levels -ERROR_CORRECT_L = 1 -ERROR_CORRECT_M = 0 -ERROR_CORRECT_Q = 3 -ERROR_CORRECT_H = 2 +ERROR_CORRECT_L: int = 1 +ERROR_CORRECT_M: int = 0 +ERROR_CORRECT_Q: int = 3 +ERROR_CORRECT_H: int = 2 diff --git a/qrcode/exceptions.py b/qrcode/exceptions.py index b37bd30c..2d2adec3 100644 --- a/qrcode/exceptions.py +++ b/qrcode/exceptions.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class DataOverflowError(Exception): pass diff --git a/qrcode/image/base.py b/qrcode/image/base.py index 119c30a6..cd41d824 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import abc -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Union from qrcode.image.styles.moduledrawers.base import QRModuleDrawer @@ -15,8 +17,8 @@ class BaseImage: Base QRCode image output class. """ - kind: Optional[str] = None - allowed_kinds: Optional[tuple[str]] = None + kind: str | None = None + allowed_kinds: tuple[str, ...] | None = None needs_context = False needs_processing = False needs_drawrect = True @@ -140,7 +142,7 @@ def __init__( def get_drawer( self, drawer: Union[QRModuleDrawer, str, None] - ) -> Optional[QRModuleDrawer]: + ) -> QRModuleDrawer | None: if not isinstance(drawer, str): return drawer drawer_cls, kwargs = self.drawer_aliases[drawer] diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 57ee13a8..4ff672bb 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import qrcode.image.base from PIL import Image, ImageDraw diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index 5a8b2c5e..abff0224 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from itertools import chain from qrcode.compat.png import PngWriter diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index cd63a6e0..bd371e39 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import qrcode.image.base from PIL import Image from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 9599f7fb..80c48891 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math from PIL import Image diff --git a/qrcode/image/styles/moduledrawers/base.py b/qrcode/image/styles/moduledrawers/base.py index 154d2cfa..4a61432e 100644 --- a/qrcode/image/styles/moduledrawers/base.py +++ b/qrcode/image/styles/moduledrawers/base.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import abc from typing import TYPE_CHECKING diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index 4aa42496..fb709efb 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from PIL import Image, ImageDraw diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 4117559a..826420c4 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import decimal from decimal import Decimal -from typing import Optional, Union, overload, Literal +from typing import Union, overload, Literal import qrcode.image.base from qrcode.compat.etree import ET @@ -81,7 +83,7 @@ class SvgImage(SvgFragmentImage): Creates a QR-code image as a standalone SVG document. """ - background: Optional[str] = None + background: str | None = None drawer_aliases: qrcode.image.base.DrawerAliases = { "circle": (svg_drawers.SvgCircleDrawer, {}), "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), @@ -121,8 +123,8 @@ class SvgPathImage(SvgImage): "stroke": "none", } - needs_processing = True - path: Optional[ET.Element] = None + needs_processing: bool = True + path: ET.Element | None = None default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer drawer_aliases = { "circle": (svg_drawers.SvgPathCircleDrawer, {}), diff --git a/qrcode/main.py b/qrcode/main.py index 152c97b0..e0afcdc2 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import sys from bisect import bisect_left from typing import ( Generic, NamedTuple, - Optional, TypeVar, cast, overload, @@ -14,7 +15,7 @@ from qrcode.image.base import BaseImage from qrcode.image.pure import PyPNGImage -ModulesType = list[list[Optional[bool]]] +ModulesType = list[list[bool | None]] # Cache modules generated just based on the QR Code version precomputed_qr_blanks: dict[int, ModulesType] = {} @@ -73,7 +74,7 @@ def __bool__(self) -> bool: class QRCode(Generic[GenericImage]): modules: ModulesType - _version: Optional[int] = None + _version: int | None = None def __init__( self, @@ -81,7 +82,7 @@ def __init__( error_correction=constants.ERROR_CORRECT_M, box_size=10, border=4, - image_factory: Optional[type[GenericImage]] = None, + image_factory: type[GenericImage] | None = None, mask_pattern=None, ): _check_box_size(box_size) diff --git a/qrcode/release.py b/qrcode/release.py index 208ac1ee..f06e73ed 100644 --- a/qrcode/release.py +++ b/qrcode/release.py @@ -3,6 +3,8 @@ qrcode versions. """ +from __future__ import annotations + import os import re import datetime diff --git a/qrcode/util.py b/qrcode/util.py index fe25548f..f68208a2 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import re From 715b0880d36f616a1d76f9077d1c987ff69aad2e Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 22 Jul 2025 14:29:58 +0200 Subject: [PATCH 3/6] Correct abstraction Setup --- qrcode/image/base.py | 2 +- qrcode/image/styledpil.py | 8 ++++++++ qrcode/image/svg.py | 6 ++++++ qrcode/tests/test_qrcode.py | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/qrcode/image/base.py b/qrcode/image/base.py index cd41d824..ef586bd2 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -12,7 +12,7 @@ DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]] -class BaseImage: +class BaseImage(abc.ABC): """ Base QRCode image output class. """ diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index bd371e39..62cb0781 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import overload + import qrcode.image.base from PIL import Image from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask @@ -69,6 +71,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @overload + def drawrect(self, row, col): + """ + Not used. + """ + def new_image(self, **kwargs): mode = ( "RGBA" diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 826420c4..318ebf9d 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -28,6 +28,12 @@ def __init__(self, *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 drawrect(self, row, col): + """ + Not used. + """ + @overload def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ... diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 65242848..efdacdf7 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -131,6 +131,7 @@ def test_qrcode_factory(): class MockFactory(BaseImage): drawrect = mock.Mock() new_image = mock.Mock() + save = mock.Mock() qr = qrcode.QRCode(image_factory=MockFactory) qr.add_data(UNICODE_TEXT) From e9030877dd9ce15d8071b8700da46cb1c804f1e9 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 22 Jul 2025 14:30:15 +0200 Subject: [PATCH 4/6] Autoformatting --- qrcode/image/styles/moduledrawers/svg.py | 45 +++++++++++++----------- qrcode/tests/test_qrcode_svg.py | 16 +++++---- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index ea16174f..0245c6aa 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import abc from decimal import Decimal from typing import TYPE_CHECKING, NamedTuple @@ -21,7 +23,7 @@ class Coords(NamedTuple): class BaseSvgQRModuleDrawer(QRModuleDrawer): - img: "SvgFragmentImage" + img: SvgFragmentImage def __init__(self, *, size_ratio: Decimal = Decimal(1), **kwargs): self.size_ratio = size_ratio @@ -97,7 +99,7 @@ def el(self, box): class SvgPathQRModuleDrawer(BaseSvgQRModuleDrawer): - img: "SvgPathImage" + img: SvgPathImage def drawrect(self, box, is_active: bool): if not is_active: @@ -148,6 +150,7 @@ class SvgRoundedModuleDrawer(SvgPathQRModuleDrawer): 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: Decimal = Decimal(1), **kwargs): @@ -161,9 +164,9 @@ def initialize(self, *args, **kwargs) -> None: def drawrect(self, box, is_active): if not is_active: return - + # Check if is_active has neighbor information (ActiveWithNeighbors object) - if hasattr(is_active, 'N'): + if hasattr(is_active, "N"): # Neighbor information is available self.img._subpaths.append(self.subpath(box, is_active)) else: @@ -178,36 +181,36 @@ def subpath_all_rounded(self, box) -> str: x1 = self.img.units(coords.x1, text=False) y1 = self.img.units(coords.y1, text=False) r = self.img.units(self.corner_radius, text=False) - + # Build the path with all corners rounded path = [] - + # Start at top-left after the rounded part path.append(f"M{x0 + r},{y0}") - + # Top edge to top-right corner path.append(f"H{x1 - r}") # Top-right rounded corner path.append(f"A{r},{r} 0 0 1 {x1},{y0 + r}") - + # Right edge to bottom-right corner path.append(f"V{y1 - r}") # Bottom-right rounded corner path.append(f"A{r},{r} 0 0 1 {x1 - r},{y1}") - + # Bottom edge to bottom-left corner path.append(f"H{x0 + r}") # Bottom-left rounded corner path.append(f"A{r},{r} 0 0 1 {x0},{y1 - r}") - + # Left edge to top-left corner path.append(f"V{y0 + r}") # Top-left rounded corner path.append(f"A{r},{r} 0 0 1 {x0 + r},{y0}") - + # Close the path path.append("z") - + return "".join(path) def subpath(self, box, is_active) -> str: @@ -217,17 +220,17 @@ def subpath(self, box, is_active) -> str: 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 - + 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) r = self.img.units(self.corner_radius, text=False) - + # Build the path path = [] - + # Start at top-left and move clockwise if nw_rounded: # Start at top-left corner, after the rounded part @@ -235,7 +238,7 @@ def subpath(self, box, is_active) -> str: else: # Start at the top-left corner path.append(f"M{x0},{y0}") - + # Top edge to top-right corner if ne_rounded: path.append(f"H{x1 - r}") @@ -243,7 +246,7 @@ def subpath(self, box, is_active) -> str: path.append(f"A{r},{r} 0 0 1 {x1},{y0 + r}") else: path.append(f"H{x1}") - + # Right edge to bottom-right corner if se_rounded: path.append(f"V{y1 - r}") @@ -251,7 +254,7 @@ def subpath(self, box, is_active) -> str: path.append(f"A{r},{r} 0 0 1 {x1 - r},{y1}") else: path.append(f"V{y1}") - + # Bottom edge to bottom-left corner if sw_rounded: path.append(f"H{x0 + r}") @@ -259,7 +262,7 @@ def subpath(self, box, is_active) -> str: path.append(f"A{r},{r} 0 0 1 {x0},{y1 - r}") else: path.append(f"H{x0}") - + # Left edge back to start if nw_rounded: path.append(f"V{y0 + r}") @@ -267,8 +270,8 @@ def subpath(self, box, is_active) -> str: path.append(f"A{r},{r} 0 0 1 {x0 + r},{y0}") else: path.append(f"V{y0}") - + # Close the path path.append("z") - + return "".join(path) diff --git a/qrcode/tests/test_qrcode_svg.py b/qrcode/tests/test_qrcode_svg.py index 20075593..90a82563 100644 --- a/qrcode/tests/test_qrcode_svg.py +++ b/qrcode/tests/test_qrcode_svg.py @@ -60,23 +60,25 @@ def test_svg_rounded_module_drawer(): """Test that the SvgRoundedModuleDrawer works correctly.""" qr = qrcode.QRCode() qr.add_data(UNICODE_TEXT) - + # Test with default parameters module_drawer = SvgRoundedModuleDrawer() img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) img.save(io.BytesIO()) - + # Test with custom radius_ratio - module_drawer = SvgRoundedModuleDrawer(radius_ratio=Decimal('0.5')) + module_drawer = SvgRoundedModuleDrawer(radius_ratio=Decimal("0.5")) img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) img.save(io.BytesIO()) - + # Test with custom size_ratio - module_drawer = SvgRoundedModuleDrawer(size_ratio=Decimal('0.8')) + module_drawer = SvgRoundedModuleDrawer(size_ratio=Decimal("0.8")) img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) img.save(io.BytesIO()) - + # Test with both custom parameters - module_drawer = SvgRoundedModuleDrawer(radius_ratio=Decimal('0.3'), size_ratio=Decimal('0.9')) + module_drawer = SvgRoundedModuleDrawer( + radius_ratio=Decimal("0.3"), size_ratio=Decimal("0.9") + ) img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer=module_drawer) img.save(io.BytesIO()) From b4c4d48fb96fe434109f2cc236f677944d268682 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 22 Jul 2025 14:30:43 +0200 Subject: [PATCH 5/6] Add rounded module to the list of CLI choices --- qrcode/image/svg.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 318ebf9d..ddd4fa53 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -94,6 +94,10 @@ class SvgImage(SvgFragmentImage): "circle": (svg_drawers.SvgCircleDrawer, {}), "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}), + "rounded-module": ( + svg_drawers.SvgRoundedModuleDrawer, + {"size_ratio": Decimal(0.8)}, + ), } def _svg(self, tag="svg", **kwargs): @@ -142,6 +146,10 @@ class SvgPathImage(SvgImage): svg_drawers.SvgPathSquareDrawer, {"size_ratio": Decimal(0.8)}, ), + "rounded-module": ( + svg_drawers.SvgRoundedModuleDrawer, + {"size_ratio": Decimal(0.8)}, + ), } def __init__(self, *args, **kwargs): From c10edcc6fc9aeb1599a59c465ea38c34b349a91b Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 22 Jul 2025 14:36:10 +0200 Subject: [PATCH 6/6] Revert "Consistent Type annotation setup, overall typo fixes" This reverts commit 318bab45cb0791aebe93c783a77eaa8db7c34488. --- qrcode/LUT.py | 10 +++++----- qrcode/__init__.py | 2 -- qrcode/base.py | 2 -- qrcode/console_scripts.py | 8 +++----- qrcode/constants.py | 10 ++++------ qrcode/exceptions.py | 3 --- qrcode/image/base.py | 10 ++++------ qrcode/image/pil.py | 2 -- qrcode/image/pure.py | 2 -- qrcode/image/styles/colormasks.py | 2 -- qrcode/image/styles/moduledrawers/base.py | 2 -- qrcode/image/styles/moduledrawers/pil.py | 2 -- qrcode/image/svg.py | 10 ++++------ qrcode/main.py | 9 ++++----- qrcode/release.py | 2 -- qrcode/util.py | 2 -- 16 files changed, 24 insertions(+), 54 deletions(-) diff --git a/qrcode/LUT.py b/qrcode/LUT.py index e0ea906f..115892f1 100644 --- a/qrcode/LUT.py +++ b/qrcode/LUT.py @@ -1,10 +1,10 @@ -""" -Store all kinds of lookup table. -""" +# 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 @@ -13,7 +13,7 @@ # 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): diff --git a/qrcode/__init__.py b/qrcode/__init__.py index 4bdbacc3..6b238d33 100644 --- a/qrcode/__init__.py +++ b/qrcode/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from qrcode.main import QRCode from qrcode.main import make # noqa from qrcode.constants import ( # noqa diff --git a/qrcode/base.py b/qrcode/base.py index 3f806317..20f81f6f 100644 --- a/qrcode/base.py +++ b/qrcode/base.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import NamedTuple from qrcode import constants diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index ad69041e..ebe8810f 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -6,12 +6,10 @@ a pipe to a file an image is written. The default image format is PNG. """ -from __future__ import annotations - import optparse import os import sys -from typing import NoReturn +from typing import NoReturn, Optional from collections.abc import Iterable from importlib import metadata @@ -124,7 +122,7 @@ def raise_error(msg: str) -> NoReturn: return kwargs = {} - aliases: DrawerAliases | None = getattr( + aliases: Optional[DrawerAliases] = getattr( qr.image_factory, "drawer_aliases", None ) if opts.factory_drawer: @@ -158,7 +156,7 @@ def get_drawer_help() -> str: image = get_factory(module) except ImportError: # pragma: no cover continue - aliases: DrawerAliases | None = getattr(image, "drawer_aliases", None) + aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None) if not aliases: continue factories = help.setdefault(commas(aliases), set()) diff --git a/qrcode/constants.py b/qrcode/constants.py index defdfab8..385dda08 100644 --- a/qrcode/constants.py +++ b/qrcode/constants.py @@ -1,7 +1,5 @@ -from __future__ import annotations - # QR error correct levels -ERROR_CORRECT_L: int = 1 -ERROR_CORRECT_M: int = 0 -ERROR_CORRECT_Q: int = 3 -ERROR_CORRECT_H: int = 2 +ERROR_CORRECT_L = 1 +ERROR_CORRECT_M = 0 +ERROR_CORRECT_Q = 3 +ERROR_CORRECT_H = 2 diff --git a/qrcode/exceptions.py b/qrcode/exceptions.py index 2d2adec3..b37bd30c 100644 --- a/qrcode/exceptions.py +++ b/qrcode/exceptions.py @@ -1,5 +1,2 @@ -from __future__ import annotations - - class DataOverflowError(Exception): pass diff --git a/qrcode/image/base.py b/qrcode/image/base.py index ef586bd2..3767b836 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -1,7 +1,5 @@ -from __future__ import annotations - import abc -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Optional, Union from qrcode.image.styles.moduledrawers.base import QRModuleDrawer @@ -17,8 +15,8 @@ class BaseImage(abc.ABC): Base QRCode image output class. """ - kind: str | None = None - allowed_kinds: tuple[str, ...] | None = None + kind: Optional[str] = None + allowed_kinds: Optional[tuple[str]] = None needs_context = False needs_processing = False needs_drawrect = True @@ -142,7 +140,7 @@ def __init__( def get_drawer( self, drawer: Union[QRModuleDrawer, str, None] - ) -> QRModuleDrawer | None: + ) -> Optional[QRModuleDrawer]: if not isinstance(drawer, str): return drawer drawer_cls, kwargs = self.drawer_aliases[drawer] diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 4ff672bb..57ee13a8 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import qrcode.image.base from PIL import Image, ImageDraw diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index abff0224..5a8b2c5e 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from itertools import chain from qrcode.compat.png import PngWriter diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 80c48891..9599f7fb 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import math from PIL import Image diff --git a/qrcode/image/styles/moduledrawers/base.py b/qrcode/image/styles/moduledrawers/base.py index 4a61432e..154d2cfa 100644 --- a/qrcode/image/styles/moduledrawers/base.py +++ b/qrcode/image/styles/moduledrawers/base.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import abc from typing import TYPE_CHECKING diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index fb709efb..4aa42496 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import TYPE_CHECKING from PIL import Image, ImageDraw diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 318ebf9d..f025f88e 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import decimal from decimal import Decimal -from typing import Union, overload, Literal +from typing import Optional, Union, overload, Literal import qrcode.image.base from qrcode.compat.etree import ET @@ -89,7 +87,7 @@ class SvgImage(SvgFragmentImage): Creates a QR-code image as a standalone SVG document. """ - background: str | None = None + background: Optional[str] = None drawer_aliases: qrcode.image.base.DrawerAliases = { "circle": (svg_drawers.SvgCircleDrawer, {}), "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), @@ -129,8 +127,8 @@ class SvgPathImage(SvgImage): "stroke": "none", } - needs_processing: bool = True - path: ET.Element | None = None + needs_processing = True + path: Optional[ET.Element] = None default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer drawer_aliases = { "circle": (svg_drawers.SvgPathCircleDrawer, {}), diff --git a/qrcode/main.py b/qrcode/main.py index e0afcdc2..152c97b0 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -1,10 +1,9 @@ -from __future__ import annotations - import sys from bisect import bisect_left from typing import ( Generic, NamedTuple, + Optional, TypeVar, cast, overload, @@ -15,7 +14,7 @@ from qrcode.image.base import BaseImage from qrcode.image.pure import PyPNGImage -ModulesType = list[list[bool | None]] +ModulesType = list[list[Optional[bool]]] # Cache modules generated just based on the QR Code version precomputed_qr_blanks: dict[int, ModulesType] = {} @@ -74,7 +73,7 @@ def __bool__(self) -> bool: class QRCode(Generic[GenericImage]): modules: ModulesType - _version: int | None = None + _version: Optional[int] = None def __init__( self, @@ -82,7 +81,7 @@ def __init__( error_correction=constants.ERROR_CORRECT_M, box_size=10, border=4, - image_factory: type[GenericImage] | None = None, + image_factory: Optional[type[GenericImage]] = None, mask_pattern=None, ): _check_box_size(box_size) diff --git a/qrcode/release.py b/qrcode/release.py index f06e73ed..208ac1ee 100644 --- a/qrcode/release.py +++ b/qrcode/release.py @@ -3,8 +3,6 @@ qrcode versions. """ -from __future__ import annotations - import os import re import datetime diff --git a/qrcode/util.py b/qrcode/util.py index f68208a2..fe25548f 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import math import re