diff --git a/CHANGES.rst b/CHANGES.rst index ff3283e1..83a7dd72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,14 +2,21 @@ Changes ======= -Deprecation Warnings -==================== +Change Log +========== -Removed in v9.0: ----------------- +WIP (9.0) +--------- + +**Backwards incompatible changes:** + +- The ``width`` and ``height`` attributes was removed from the ```` 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. -- Importing a PIL drawer from ``qrcode.image.styles.moduledrawers`` has been deprecated. - Update your code to import directly from the ``pil`` module instead: +- Importing a PIL drawer from ``qrcode.image.styles.moduledrawers`` is no longer + supported. Update your code to import directly from the ``pil`` module instead: .. code-block:: python @@ -17,7 +24,7 @@ Removed in v9.0: from qrcode.image.styles.moduledrawers.pil import SquareModuleDrawer # New - Calling ``QRCode.make_image`` or ``StyledPilImage`` with the arguments ``embeded_image`` - or ``embeded_image_path`` have been deprecated due to typographical errors. Update + or ``embeded_image_path`` has been removed to typographical errors. Update your code to use the correct arguments ``embedded_image`` and ``embededd_image_path``: .. code-block:: python @@ -29,14 +36,6 @@ 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 ```` 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. - -Change Log -========== - WIP 8.x ------- diff --git a/pyproject.toml b/pyproject.toml index 9d56b383..8bad19c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "qrcode" -version = "8.2" +version = "9.0a0" description = "QR Code image generator" authors = [ { name = "Lincoln Loop", email = "info@lincolnloop.com" }, @@ -31,7 +31,6 @@ classifiers = [ requires-python = "~=3.9" dependencies = [ "colorama; sys_platform == 'win32'", - "deprecation", ] diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index 17f947f4..8bb6719a 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -1,9 +1,7 @@ from __future__ import annotations -import warnings from typing import overload -import deprecation from PIL import Image import qrcode.image.base @@ -49,26 +47,12 @@ class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): def __init__(self, *args, **kwargs): self.color_mask = kwargs.get("color_mask", SolidFillColorMask()) - if kwargs.get("embeded_image_path") or kwargs.get("embeded_image"): - warnings.warn( - "The 'embeded_*' parameters are deprecated. Use 'embedded_image_path' " - "or 'embedded_image' instead. The 'embeded_*' parameters will be " - "removed in v9.0.", - category=DeprecationWarning, - stacklevel=2, - ) - # allow embeded_ parameters with typos for backwards compatibility - embedded_image_path = kwargs.get( - "embedded_image_path", kwargs.get("embeded_image_path") - ) - self.embedded_image = kwargs.get("embedded_image", kwargs.get("embeded_image")) - self.embedded_image_ratio = kwargs.get( - "embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25) - ) + embedded_image_path = kwargs.get("embedded_image_path") + self.embedded_image = kwargs.get("embedded_image") + self.embedded_image_ratio = kwargs.get("embedded_image_ratio", 0.25) self.embedded_image_resample = kwargs.get( - "embedded_image_resample", - kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS), + "embedded_image_resample", Image.Resampling.LANCZOS ) if not self.embedded_image and embedded_image_path: self.embedded_image = Image.open(embedded_image_path) @@ -111,15 +95,6 @@ def process(self): if self.embedded_image: self.draw_embedded_image() - @deprecation.deprecated( - deprecated_in="9.0", - removed_in="8.3", - current_version="8.2", - details="Use draw_embedded_image() instead", - ) - def draw_embeded_image(self): - return self.draw_embedded_image() - def draw_embedded_image(self): if not self.embedded_image: return diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index 056da6df..e69de29b 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -1,46 +0,0 @@ -""" -Module for lazy importing of PIL drawers with a deprecation warning. - -Currently, importing a PIL drawer from this module is allowed for backwards -compatibility but will raise a DeprecationWarning. - -This will be removed in v9.0. -""" - -import warnings - -from qrcode.constants import PIL_AVAILABLE - - -def __getattr__(name): - """Lazy import with deprecation warning for PIL drawers.""" - # List of PIL drawer names that should trigger deprecation warnings - pil_drawers = { - "CircleModuleDrawer", - "GappedCircleModuleDrawer", - "GappedSquareModuleDrawer", - "HorizontalBarsDrawer", - "RoundedModuleDrawer", - "SquareModuleDrawer", - "VerticalBarsDrawer", - } - - if name in pil_drawers: - # Only render a warning if PIL is actually installed. Otherwise it would - # raise an ImportError directly, which is fine. - if PIL_AVAILABLE: - warnings.warn( - f"Importing '{name}' directly from this module is deprecated." - f"Please use 'from qrcode.image.styles.moduledrawers.pil import {name}' " - f"instead. This backwards compatibility import will be removed in v9.0.", - DeprecationWarning, - stacklevel=2, - ) - - # Import and return the drawer from the pil module - from . import pil # noqa: PLC0415 - - return getattr(pil, name) - - # For any other attribute, raise AttributeError - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 356f4f43..6b0ea1a8 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -42,13 +42,20 @@ 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: @@ -56,7 +63,8 @@ def units(self, pixels, text=True): 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) @@ -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, ) diff --git a/qrcode/main.py b/qrcode/main.py index be62de81..ef865ac4 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -1,7 +1,6 @@ from __future__ import annotations import sys -import warnings from bisect import bisect_left from typing import Generic, Literal, NamedTuple, Optional, TypeVar, cast, overload @@ -335,22 +334,8 @@ def make_image(self, image_factory=None, **kwargs): If the data has not been compiled yet, make it first. """ - # Raise a warning that 'embeded' is still used - if kwargs.get("embeded_image_path") or kwargs.get("embeded_image"): - warnings.warn( - "The 'embeded_*' parameters are deprecated. Use 'embedded_image_path' " - "or 'embedded_image' instead. The 'embeded_*' parameters will be " - "removed in v9.0.", - category=DeprecationWarning, - stacklevel=2, - ) - - # allow embeded_ parameters with typos for backwards compatibility if ( - kwargs.get("embedded_image_path") - or kwargs.get("embedded_image") - or kwargs.get("embeded_image_path") - or kwargs.get("embeded_image") + kwargs.get("embedded_image_path") or kwargs.get("embedded_image") ) and self.error_correction != constants.ERROR_CORRECT_H: raise ValueError( "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" diff --git a/qrcode/tests/regression/test_svg_dimension.py b/qrcode/tests/regression/test_svg_dimension.py new file mode 100644 index 00000000..714058aa --- /dev/null +++ b/qrcode/tests/regression/test_svg_dimension.py @@ -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_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" diff --git a/qrcode/tests/test_deprecation.py b/qrcode/tests/test_deprecation.py deleted file mode 100644 index 5abb45cc..00000000 --- a/qrcode/tests/test_deprecation.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -import pytest - -from qrcode.constants import ERROR_CORRECT_H, PIL_AVAILABLE -from qrcode.main import QRCode - -if TYPE_CHECKING: - from tempfile import NamedTemporaryFile - - -@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") -def test_moduledrawer_import() -> None: - """ - Importing a drawer from qrcode.image.styles.moduledrawers is deprecated - and will raise a DeprecationWarning. - - Removed in v9.0. - """ - # These module imports are fine to import - from qrcode.image.styles.moduledrawers import base, pil, svg - - with pytest.warns( - DeprecationWarning, - match="Importing 'SquareModuleDrawer' directly from this module is deprecated.", - ): - from qrcode.image.styles.moduledrawers import ( - SquareModuleDrawer, - ) - - -@pytest.mark.skipif(PIL_AVAILABLE, reason="PIL is installed") -def test_moduledrawer_import_pil_not_installed() -> None: - """ - Importing from qrcode.image.styles.moduledrawers is deprecated, however, - if PIL is not installed, there will be no (false) warning; it's a simple - ImportError. - - Removed in v9.0. - """ - # These module imports are fine to import - from qrcode.image.styles.moduledrawers import base, svg - - # Importing a backwards compatible module drawer does normally render a - # DeprecationWarning; however, since PIL is not installed, it will raise an - # ImportError. - with pytest.raises(ImportError): - from qrcode.image.styles.moduledrawers import SquareModuleDrawer - - -@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") -def test_make_image_embeded_parameters() -> None: - """ - Using 'embeded_image_path' or 'embeded_image' parameters with QRCode.make_image() - is deprecated and will raise a DeprecationWarning. - - Removed in v9.0. - """ - - # Create a QRCode required for embedded images - qr = QRCode(error_correction=ERROR_CORRECT_H) - qr.add_data("test") - - # Test with embeded_image_path parameter - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated" - ): - qr.make_image(embeded_image_path="dummy_path") - - # Test with embeded_image parameter - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." - ): - qr.make_image(embeded_image="dummy_image") - - -@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") -def test_styledpilimage_embeded_parameters(dummy_image: NamedTemporaryFile) -> None: - """ - Using 'embeded_image_path' or 'embeded_image' parameters with StyledPilImage - is deprecated and will raise a DeprecationWarning. - - Removed in v9.0. - """ - from PIL import Image - - from qrcode.image.styledpil import StyledPilImage - - styled_kwargs = { - "border": 4, - "width": 21, - "box_size": 10, - "qrcode_modules": 1, - } - - # Test with embeded_image_path parameter - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." - ): - StyledPilImage(embeded_image_path=dummy_image.name, **styled_kwargs) - - # Test with embeded_image parameter - embedded_img = Image.open(dummy_image.name) - - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." - ): - StyledPilImage(embeded_image=embedded_img, **styled_kwargs)