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/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index cf5b9e7d..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: @@ -137,3 +139,139 @@ 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/image/svg.py b/qrcode/image/svg.py index 318ebf9d..a913fd51 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,11 +87,15 @@ 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)}), "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): @@ -129,8 +131,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, {}), @@ -142,6 +144,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): 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/tests/test_qrcode_svg.py b/qrcode/tests/test_qrcode_svg.py index 4774b245..90a82563 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,31 @@ 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()) 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