unvendorable qrcodegen (#887);

move copyparty-original code to qrkode.py

stolen/qrcodegen.py can be deleted and replaced with system lib

this is safe and has minimal affect on functionality;
performance will be a tiny bit slower without the vendored copy
This commit is contained in:
ed 2025-10-04 21:36:41 +00:00
parent 656f0a6c39
commit 08ebb0b4c9
6 changed files with 122 additions and 71 deletions

View file

@ -3000,6 +3000,7 @@ force use of system modules instead of the vendored versions:
| -------------------- | ------------ | | -------------------- | ------------ |
| `PRTY_SYS_ALL` | all of the below | | `PRTY_SYS_ALL` | all of the below |
| `PRTY_SYS_IFADDR` | replace [stolen/ifaddr](./copyparty/stolen/ifaddr) with [upstream](https://pypi.org/project/ifaddr/) | | `PRTY_SYS_IFADDR` | replace [stolen/ifaddr](./copyparty/stolen/ifaddr) with [upstream](https://pypi.org/project/ifaddr/) |
| `PRTY_SYS_QRCG` | replace [stolen/qrcodegen.py](./copyparty/stolen/qrcodegen.py) with [upstream](https://github.com/nayuki/QR-Code-generator/blob/master/python/qrcodegen.py) |
to debug, run copyparty with `PRTY_MODSPEC=1` to see where it's getting each module from to debug, run copyparty with `PRTY_MODSPEC=1` to see where it's getting each module from

View file

@ -34,8 +34,8 @@ from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION from .__version__ import S_VERSION
from .authsrv import LEELOO_DALLAS, VFS # typechk from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos from .bos import bos
from .qrkode import QrCode, qr2svg, qrgen
from .star import StreamTar from .star import StreamTar
from .stolen.qrcodegen import QrCode, qr2svg
from .sutil import StreamArc, gfilter from .sutil import StreamArc, gfilter
from .szip import StreamZip from .szip import StreamZip
from .up2k import up2k_chunksize from .up2k import up2k_chunksize
@ -4951,7 +4951,7 @@ class HttpCli(object):
url += "#" + uhash url += "#" + uhash
self.log("qrcode(%r)" % (url,)) self.log("qrcode(%r)" % (url,))
ret = qr2svg(QrCode.encode_binary(url.encode("utf-8")), 2) ret = qr2svg(qrgen(url.encode("utf-8")), 2)
self.reply(ret.encode("utf-8"), mime="image/svg+xml") self.reply(ret.encode("utf-8"), mime="image/svg+xml")
return True return True

112
copyparty/qrkode.py Normal file
View file

@ -0,0 +1,112 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_QRCG"):
raise ImportError()
from .stolen.qrcodegen import QrCode
qrgen = QrCode.encode_binary
VENDORED = True
except ImportError:
VENDORED = False
from qrcodegen import QrCode
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Optional, Sequence, Union
if not VENDORED:
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
ret = None
V = QrCode.Ecc
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
qr = QrCode.encode_binary(data, e)
qr.size = qr._size
qr.modules = qr._modules
if not ret or ret.size > qr.size:
ret = qr
return ret
qrgen = _qrgen
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
tab = qr.modules
sz = qr.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def qr2png(
qr: QrCode,
zoom: int,
pad: int,
bg: Optional[tuple[int, int, int]],
fg: Optional[tuple[int, int, int]],
ap: str,
) -> None:
from PIL import Image
tab = qr.modules
sz = qr.size
psz = sz + pad * 2
if bg:
img = Image.new("RGB", (psz, psz), bg)
else:
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
fg = (fg[0], fg[1], fg[2], 255)
for y in range(sz):
for x in range(sz):
if tab[y][x]:
img.putpixel((x + pad, y + pad), fg)
if zoom != 1:
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
img.save(ap)
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View file

@ -4,7 +4,7 @@
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py # https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
# the original ^ is extremely well commented so refer to that for explanations # the original ^ is extremely well commented so refer to that for explanations
# hacks: binary-only, auto-ecc, render, py2-compat # hacks: binary-only, auto-ecc, py2-compat
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
@ -173,52 +173,6 @@ class QrCode(object):
self._apply_mask(msk) # Apply the final choice of mask self._apply_mask(msk) # Apply the final choice of mask
self._draw_format_bits(msk) # Overwrite old format bits self._draw_format_bits(msk) # Overwrite old format bits
def render(self, zoom=1, pad=4) -> str:
tab = self.modules
sz = self.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def to_png(self, zoom, pad, bg, fg, ap) -> None:
from PIL import Image
tab = self.modules
sz = self.size
psz = sz + pad * 2
if bg:
img = Image.new("RGB", (psz, psz), bg)
else:
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
fg = (fg[0], fg[1], fg[2], 255)
for y in range(sz):
for x in range(sz):
if tab[y][x]:
img.putpixel((x + pad, y + pad), fg)
if zoom != 1:
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
img.save(ap)
def _draw_function_patterns(self) -> None: def _draw_function_patterns(self) -> None:
# Draw horizontal and vertical timing patterns # Draw horizontal and vertical timing patterns
for i in range(self.size): for i in range(self.size):
@ -613,20 +567,3 @@ def _get_bit(x: int, i: int) -> bool:
class DataTooLongError(ValueError): class DataTooLongError(ValueError):
pass pass
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View file

@ -9,7 +9,7 @@ import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert from .cert import gencert
from .stolen.qrcodegen import QrCode, qr2svg from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
from .util import ( from .util import (
E_ACCESS, E_ACCESS,
E_ADDR_IN_USE, E_ADDR_IN_USE,
@ -629,7 +629,7 @@ class TcpSrv(object):
pad = self.args.qrp pad = self.args.qrp
zoom = self.args.qrz zoom = self.args.qrz
qrc = QrCode.encode_binary(btxt) qrc = qrgen(btxt)
for zs in self.args.qr_file or []: for zs in self.args.qr_file or []:
self._qr2file(qrc, zs) self._qr2file(qrc, zs)
@ -642,7 +642,7 @@ class TcpSrv(object):
except: except:
zoom = 1 zoom = 1
qr = qrc.render(zoom, pad) qr = qr2txt(qrc, zoom, pad)
if self.args.no_ansi: if self.args.no_ansi:
return "{}\n{}".format(txt, qr) return "{}\n{}".format(txt, qr)
@ -683,12 +683,12 @@ class TcpSrv(object):
if zoom not in (1, 2): if zoom not in (1, 2):
raise Exception("invalid zoom for qr.txt; must be 1 or 2") raise Exception("invalid zoom for qr.txt; must be 1 or 2")
with open(ap, "wb") as f: with open(ap, "wb") as f:
f.write(qrc.render(zoom, pad).encode("utf-8")) f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
elif ap.endswith(".svg"): elif ap.endswith(".svg"):
with open(ap, "wb") as f: with open(ap, "wb") as f:
f.write(qr2svg(qrc, pad).encode("utf-8")) f.write(qr2svg(qrc, pad).encode("utf-8"))
else: else:
qrc.to_png(zoom, pad, self._h2i(bg), self._h2i(fg), ap) qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
def _h2i(self, hs): def _h2i(self, hs):
try: try:

View file

@ -25,6 +25,7 @@ copyparty/metrics.py,
copyparty/mtag.py, copyparty/mtag.py,
copyparty/multicast.py, copyparty/multicast.py,
copyparty/pwhash.py, copyparty/pwhash.py,
copyparty/qrkode.py,
copyparty/res, copyparty/res,
copyparty/res/__init__.py, copyparty/res/__init__.py,
copyparty/res/COPYING.txt, copyparty/res/COPYING.txt,