Skip to content

Feature: Add SvgRoundedModuleDrawer class for SVG QR codes with rounded corners #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions qrcode/image/styles/moduledrawers/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
28 changes: 28 additions & 0 deletions qrcode/tests/test_qrcode_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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())