diff --git a/README.md b/README.md index 121ea63e..707b1221 100644 --- a/README.md +++ b/README.md @@ -3000,6 +3000,7 @@ force use of system modules instead of the vendored versions: | -------------------- | ------------ | | `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_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 diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 77778547..1638f892 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -34,8 +34,8 @@ from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION from .authsrv import LEELOO_DALLAS, VFS # typechk from .bos import bos +from .qrkode import QrCode, qr2svg, qrgen from .star import StreamTar -from .stolen.qrcodegen import QrCode, qr2svg from .sutil import StreamArc, gfilter from .szip import StreamZip from .up2k import up2k_chunksize @@ -4951,7 +4951,7 @@ class HttpCli(object): url += "#" + uhash 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") return True diff --git a/copyparty/qrkode.py b/copyparty/qrkode.py new file mode 100644 index 00000000..34ebbcca --- /dev/null +++ b/copyparty/qrkode.py @@ -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 = """\ + + + + + +""" + return t.format(qr.size + border * 2, " ".join(parts)) diff --git a/copyparty/stolen/qrcodegen.py b/copyparty/stolen/qrcodegen.py index 8790e859..937dc88e 100644 --- a/copyparty/stolen/qrcodegen.py +++ b/copyparty/stolen/qrcodegen.py @@ -4,7 +4,7 @@ # https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py # 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 @@ -173,52 +173,6 @@ class QrCode(object): self._apply_mask(msk) # Apply the final choice of mask 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: # Draw horizontal and vertical timing patterns for i in range(self.size): @@ -613,20 +567,3 @@ def _get_bit(x: int, i: int) -> bool: class DataTooLongError(ValueError): 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 = """\ - - - - - -""" - return t.format(qr.size + border * 2, " ".join(parts)) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 72638d23..a68bf376 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -9,7 +9,7 @@ import time from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .cert import gencert -from .stolen.qrcodegen import QrCode, qr2svg +from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen from .util import ( E_ACCESS, E_ADDR_IN_USE, @@ -629,7 +629,7 @@ class TcpSrv(object): pad = self.args.qrp zoom = self.args.qrz - qrc = QrCode.encode_binary(btxt) + qrc = qrgen(btxt) for zs in self.args.qr_file or []: self._qr2file(qrc, zs) @@ -642,7 +642,7 @@ class TcpSrv(object): except: zoom = 1 - qr = qrc.render(zoom, pad) + qr = qr2txt(qrc, zoom, pad) if self.args.no_ansi: return "{}\n{}".format(txt, qr) @@ -683,12 +683,12 @@ class TcpSrv(object): if zoom not in (1, 2): raise Exception("invalid zoom for qr.txt; must be 1 or 2") 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"): with open(ap, "wb") as f: f.write(qr2svg(qrc, pad).encode("utf-8")) 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): try: diff --git a/scripts/sfx.ls b/scripts/sfx.ls index 66bcf9df..796d7d21 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -25,6 +25,7 @@ copyparty/metrics.py, copyparty/mtag.py, copyparty/multicast.py, copyparty/pwhash.py, +copyparty/qrkode.py, copyparty/res, copyparty/res/__init__.py, copyparty/res/COPYING.txt,