From d818dd539a8edce090b701d220cba785e3c95815 Mon Sep 17 00:00:00 2001 From: Guus Bertens Date: Fri, 31 Mar 2023 18:34:09 +0200 Subject: [PATCH 1/9] raise DataOverflowError instead of ValueError Fixes #322. --- qrcode/main.py | 9 +++++---- qrcode/tests/test_qrcode.py | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/qrcode/main.py b/qrcode/main.py index 0ac91bbb..cfc1e3ce 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -156,8 +156,8 @@ def make(self, fit=True): :param fit: If ``True`` (or if a size has not been provided), find the best fit for the data to avoid data overflow errors. """ - if fit or (self.version is None): - self.best_fit(start=self.version) + if fit or (self._version is None): + self.best_fit(start=self._version) if self.mask_pattern is None: self.makeImpl(False, self.best_mask_pattern()) else: @@ -229,11 +229,12 @@ def best_fit(self, start=None): data.write(buffer) needed_bits = len(buffer) - self.version = bisect_left( + new_version = bisect_left( util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start ) - if self.version == 41: + if new_version == 41: raise exceptions.DataOverflowError() + self.version = new_version # Now check whether we need more bits for the mode sizes, recursing if # our guess was too low diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 5c1ea35b..27d0a329 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -66,6 +66,12 @@ def test_fit(self): qr.make() self.assertEqual(qr.version, 2) + def test_fit_overflow(self): + # Alphanumeric. Version 40 with ERROR_CORRECT_LOW has max 4296 characters. + qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L) + qr.add_data("A" * 4297) + self.assertRaises(DataOverflowError, qr.make) + def test_mode_number(self): qr = qrcode.QRCode() qr.add_data("1234567890123456789012345678901234", optimize=0) From b92aa7f2923aa1b5feacfc2f0848937292ab47d9 Mon Sep 17 00:00:00 2001 From: Guus Bertens Date: Fri, 31 Mar 2023 18:34:56 +0200 Subject: [PATCH 2/9] make qr print a nicer error message on too large inputs Fixes #273. --- qrcode/console_scripts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 424fe6fd..a9f23751 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -110,6 +110,11 @@ def raise_error(msg: str) -> NoReturn: else: qr.add_data(data, optimize=opts.optimize) + try: + qr.make() + except qrcode.exceptions.DataOverflowError: + raise_error("too much data to fit in QR code") + if opts.output: img = qr.make_image() with open(opts.output, "wb") as out: From 820eacf9c67cf5353953a5a8541f11aeed8c510c Mon Sep 17 00:00:00 2001 From: Alexander Smirnov <145155732+smalyu@users.noreply.github.com> Date: Mon, 2 Sep 2024 23:59:30 +0500 Subject: [PATCH 3/9] Optimize QRColorMask apply_mask method for enhanced performance This commit introduces optimizations to the apply_mask method in the QRColorMask class to improve performance. Changes include: 1. Replacing getpixel and putpixel with direct pixel manipulation using the load() method, which speeds up the process. 2. Implementing a caching mechanism to reuse color transformations for identical pixel colors, reducing redundant calculations. 3. Adding conditions to skip processing for background color pixels to reduce computational load. These optimizations have significantly reduced the method's execution time. In some experiments, these changes have resulted in performance improvements of over ten times compared to the original method, especially for larger images. --- qrcode/image/styles/colormasks.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 3b9a8084..cb3815ef 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -32,22 +32,27 @@ def initialize(self, styledPilImage, image): def apply_mask(self, image): width, height = image.size + pixels = image.load() + fg_color_cache = {} for x in range(width): for y in range(height): - norm = self.extrap_color( - self.back_color, self.paint_color, image.getpixel((x, y)) - ) + current_color = pixels[x, y] + if current_color == self.back_color: + continue + if current_color in fg_color_cache: + pixels[x, y] = fg_color_cache[current_color] + continue + norm = self.extrap_color(self.back_color, self.paint_color, current_color) if norm is not None: - image.putpixel( - (x, y), - self.interp_color( - self.get_bg_pixel(image, x, y), - self.get_fg_pixel(image, x, y), - norm, - ), + new_color = self.interp_color( + self.get_bg_pixel(image, x, y), + self.get_fg_pixel(image, x, y), + norm ) + pixels[x, y] = new_color + fg_color_cache[current_color] = new_color else: - image.putpixel((x, y), self.get_bg_pixel(image, x, y)) + pixels[x, y] = self.get_bg_pixel(image, x, y) def get_fg_pixel(self, image, x, y): raise NotImplementedError("QRModuleDrawer.paint_fg_pixel") From 13c8614f64c1285611a0ba8fec1edff6fb96a19e Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 3 Oct 2024 12:28:31 -0300 Subject: [PATCH 4/9] Back to development: 8.1 --- CHANGES.rst | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index cdbd9f27..01b8e3ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Change log ========== +8.1 (unreleased) +================== + +- Nothing changed yet. + + 8.0 ( 27 September 2024) ======================== diff --git a/pyproject.toml b/pyproject.toml index 2ee21650..81fb044a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "qrcode" -version = "8.0" +version = "8.1.dev0" packages = [{include = "qrcode"}] description = "QR Code image generator" authors = ["Lincoln Loop "] From 4a5141c2bbf0752e5a16e6038367e95dd206f27b Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Wed, 2 Oct 2024 10:33:14 -0300 Subject: [PATCH 5/9] Default to SVG on console script when pil and png are not installed --- README.rst | 13 ++++++++++--- qrcode/image/pil.py | 5 ++++- qrcode/main.py | 11 +++++++---- qrcode/tests/test_script.py | 6 +----- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index ec383462..b7eb3fcd 100644 --- a/README.rst +++ b/README.rst @@ -4,15 +4,17 @@ Pure python QR Code generator Generate QR codes. -A standard install uses pypng_ to generate PNG files and can also render QR +A standard install generates SVG files and can also render QR codes directly to the console. A standard install is just:: pip install qrcode -For more image functionality, install qrcode with the ``pil`` dependency so -that pillow_ is installed and can be used for generating images:: +For more image functionality, install qrcode with the ``pil`` or ``png`` +dependencies so that pillow_ or pypng_ is installed and can be used for +generating images:: pip install "qrcode[pil]" + pip install "qrcode[png]" .. _pypng: https://pypi.python.org/pypi/pypng .. _pillow: https://pypi.python.org/pypi/Pillow @@ -32,6 +34,11 @@ Usage From the command line, use the installed ``qr`` script:: + qr "Some text" > test.svg + +If you installed the ``pil`` or ``png`` dependencies, those will be used by +default by the script and the output will be a PNG image:: + qr "Some text" > test.png Or in Python, use the ``make`` shortcut function: diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 57ee13a8..41591629 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,6 +1,9 @@ import qrcode.image.base -from PIL import Image, ImageDraw +try: + from PIL import Image, ImageDraw +except ImportError: + Image, ImageDraw = None, None class PilImage(qrcode.image.base.BaseImage): """ diff --git a/qrcode/main.py b/qrcode/main.py index 46116b5c..f0dba089 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -4,18 +4,17 @@ Dict, Generic, List, + Literal, NamedTuple, Optional, Type, TypeVar, cast, overload, - Literal, ) from qrcode import constants, exceptions, util from qrcode.image.base import BaseImage -from qrcode.image.pure import PyPNGImage ModulesType = List[List[Optional[bool]]] # Cache modules generated just based on the QR Code version @@ -361,9 +360,13 @@ def make_image(self, image_factory=None, **kwargs): image_factory = self.image_factory if image_factory is None: from qrcode.image.pil import Image, PilImage + from qrcode.image.pure import PngWriter, PyPNGImage + from qrcode.image.svg import SvgImage - # Use PIL by default if available, otherwise use PyPNG. - image_factory = PilImage if Image else PyPNGImage + # Use PIL by default if available, otherwise use PyPNG or SVG + image_factory = ( + PilImage if Image else PyPNGImage if PngWriter else SvgImage + ) im = image_factory( self.border, diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index d6338ded..406a800d 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -19,7 +19,6 @@ def test_isatty(mock_print_ascii): @mock.patch("os.isatty", lambda *args: False) def test_piped(): - pytest.importorskip("PIL", reason="Requires PIL") main(["testtext"]) @@ -48,7 +47,6 @@ def test_stdin_py3_unicodedecodeerror(): def test_optimize(): - pytest.importorskip("PIL", reason="Requires PIL") main("testtext --optimize 0".split()) @@ -63,13 +61,11 @@ def test_bad_factory(): @mock.patch.object(sys, "argv", "qr testtext output".split()) def test_sys_argv(): - pytest.importorskip("PIL", reason="Requires PIL") main() def test_output(tmp_path): - pytest.importorskip("PIL", reason="Requires PIL") - main(["testtext", "--output", str(tmp_path / "test.png")]) + main(["testtext", "--output", str(tmp_path / "test.svg")]) def test_factory_drawer_none(capsys): From 815122b0766bb757888c65e3410d8d9d51cee8ed Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 3 Oct 2024 12:29:29 -0300 Subject: [PATCH 6/9] Add doc folder to sdist removed after Poetry migration --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 81fb044a..3978fad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "poetry.core.masonry.api" name = "qrcode" version = "8.1.dev0" packages = [{include = "qrcode"}] +include = [{ path = "doc", format = "sdist" }] description = "QR Code image generator" authors = ["Lincoln Loop "] license = "BSD" From 9a2205c52844f6741c33c860c296d224ab8ec914 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 3 Oct 2024 12:47:49 -0300 Subject: [PATCH 7/9] Update console script help message with default file format --- qrcode/console_scripts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 6c829a50..d44216e9 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -3,7 +3,8 @@ qr - Convert stdin (or the first argument) to a QR Code. When stdout is a tty the QR Code is printed to the terminal and when stdout is -a pipe to a file an image is written. The default image format is PNG. +a pipe to a file an image is written. The default image format is PNG if pil or +png optional dependencies are installed, otherwise SVG. """ import optparse From f9f217acd36976cadabb3a20c64d03cabd48aecd Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 3 Oct 2024 13:02:21 -0300 Subject: [PATCH 8/9] Ruff format --- qrcode/image/pil.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 41591629..cfb1de66 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -5,6 +5,7 @@ except ImportError: Image, ImageDraw = None, None + class PilImage(qrcode.image.base.BaseImage): """ PIL image builder, default format is PNG. From a6d63478e4abc1efea22e7e35c3da26b662c9c74 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 3 Oct 2024 15:16:20 -0300 Subject: [PATCH 9/9] Ruff format --- qrcode/image/styles/colormasks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 495a9c71..5d42bea3 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -39,12 +39,14 @@ def apply_mask(self, image): if current_color in fg_color_cache: pixels[x, y] = fg_color_cache[current_color] continue - norm = self.extrap_color(self.back_color, self.paint_color, current_color) + norm = self.extrap_color( + self.back_color, self.paint_color, current_color + ) if norm is not None: new_color = self.interp_color( self.get_bg_pixel(image, x, y), self.get_fg_pixel(image, x, y), - norm + norm, ) pixels[x, y] = new_color fg_color_cache[current_color] = new_color