Skip to content
Closed
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
12 changes: 12 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,21 @@ Removed in v9.0:
StyledPilImage(embeded_image=..., embeded_image_path=...) # Old
StyledPilImage(embedded_image=..., embedded_image_path=...) # New

- The ``width`` and ``height`` attributes will be removed from the ``<svg>``tag.
Instead, the``viewBox`` attribute is now used for defining the dimensions.
Additionally, all SVG elements now utilize pixel units rather than millimeters,
which may cause rendering differences in browsers. (`#351`_)

.. _#351: https://github.com/lincolnloop/python-qrcode/issues/351

Change Log
==========

WIP (9.0)
---------

- **Removed** ``width=.. height=...`` attributes from SVG tag, using viewBox instead. SVG elements now use pixel units instead of millimeters.

WIP
---

Expand Down
22 changes: 15 additions & 7 deletions qrcode/image/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,29 @@ def units(self, pixels: int | Decimal, text: Literal[False]) -> Decimal: ...
@overload
def units(self, pixels: int | Decimal, text: Literal[True] = True) -> str: ...

def units(self, pixels, text=True):
def units(self, pixels: int, text=True) -> Decimal | str:
"""
A box_size of 10 (default) equals 1mm.
Converts pixel values into a decimal representation with up to three decimal
places of precision or a string representation, optionally rounding to
lower precision without data loss.
"""
units = Decimal(pixels) / 10
units = Decimal(pixels)
if not text:
return units

# Round the decimal to 3 decimal places first, then try to reduce precision
# further by attempting to round to 2 decimals, 1 decimal, and whole numbers.
# If any rounding causes data loss (raises Inexact), keep the previous
# precision.
units = units.quantize(Decimal("0.001"))
context = decimal.Context(traps=[decimal.Inexact])
try:
for d in (Decimal("0.01"), Decimal("0.1"), Decimal(0)):
units = units.quantize(d, context=context)
except decimal.Inexact:
pass
return f"{units}mm"

return str(units)

def save(self, stream, kind=None):
self.check_kind(kind=kind)
Expand All @@ -71,11 +79,11 @@ def new_image(self, **kwargs):
def _svg(self, tag=None, version="1.1", **kwargs):
if tag is None:
tag = ET.QName(self._SVG_namespace, "svg")
dimension = self.units(self.pixel_size)
dimension = self.units(self.pixel_size, text=False)
viewBox = kwargs.get("viewBox", f"0 0 {dimension} {dimension}")
kwargs["viewBox"] = viewBox
return ET.Element(
tag,
width=dimension,
height=dimension,
version=version,
**kwargs,
)
Expand Down
Empty file.
51 changes: 51 additions & 0 deletions qrcode/tests/regression/test_svg_dimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import io
import re
from typing import TYPE_CHECKING

import pytest

import qrcode
from qrcode.image import svg
from qrcode.tests.consts import UNICODE_TEXT

if TYPE_CHECKING:
from qrcode.image.base import BaseImageWithDrawer


@pytest.mark.parametrize(
"image_factory",
[
svg.SvgFragmentImage,
svg.SvgImage,
svg.SvgFillImage,
svg.SvgPathImage,
svg.SvgPathFillImage,
],
)
def test_svg_no_width_height(image_factory: BaseImageWithDrawer) -> None:
"""Test that SVG output doesn't have width and height attributes."""
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)

# Create a svg with the specified factory and (optional) module drawer
img = qr.make_image(image_factory=image_factory)
svg_str = img.to_string().decode("utf-8")

# Check that width and height attributes are not present in the SVG tag
svg_tag_match = re.search(r"<svg[^>]*>", svg_str)
assert svg_tag_match, "SVG tag not found"

svg_tag = svg_tag_match.group(0)
assert "width=" not in svg_tag, "width attribute should not be present"
assert "height=" not in svg_tag, "height attribute should not be present"

# Check that viewBox is present and uses pixels (no mm suffix)
viewbox_match = re.search(r'viewBox="([^"]*)"', svg_tag)
assert viewbox_match, "viewBox attribute not found"
viewbox = viewbox_match.group(1)
assert "mm" not in viewbox, "viewBox should use pixels, not mm"

# Check that inner elements use pixels (no mm suffix)
assert "mm" not in svg_str, "SVG elements should use pixels, not mm"