From 202ddeac0df83b53a2a3d84e49889259325ace2d Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 21 Aug 2025 23:06:39 +0000 Subject: [PATCH] write qrcode to file --- copyparty/__main__.py | 1 + copyparty/stolen/qrcodegen.py | 19 +++++++++++++++++++ copyparty/tcpsrv.py | 32 +++++++++++++++++++++++++++++++- tests/util.py | 2 +- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 8467fd81..3cb97949 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1115,6 +1115,7 @@ def add_qr(ap, tty): ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log") + ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent") def add_fs(ap): diff --git a/copyparty/stolen/qrcodegen.py b/copyparty/stolen/qrcodegen.py index 38296359..8790e859 100644 --- a/copyparty/stolen/qrcodegen.py +++ b/copyparty/stolen/qrcodegen.py @@ -200,6 +200,25 @@ class QrCode(object): 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): diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index ff019caa..8d355251 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 +from .stolen.qrcodegen import QrCode, qr2svg from .util import ( E_ACCESS, E_ADDR_IN_USE, @@ -621,6 +621,10 @@ class TcpSrv(object): pad = self.args.qrp zoom = self.args.qrz qrc = QrCode.encode_binary(btxt) + + for zs in self.args.qr_file or []: + self._qr2file(qrc, zs) + if zoom == 0: try: tw, th = termsize() @@ -656,3 +660,29 @@ class TcpSrv(object): t = t.replace("\n", "`\n`") return txt + t + + def _qr2file(self, qrc: QrCode, txt: str): + if ".txt:" in txt or ".svg:" in txt: + ap, zs1, zs2 = txt.rsplit(":", 2) + bg = fg = "" + else: + ap, zs1, zs2, bg, fg = txt.rsplit(":", 4) + zoom = int(zs1) + pad = int(zs2) + + if ap.endswith(".txt"): + 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")) + 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) + + def _h2i(self, hs): + try: + return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4)) + except: + return None diff --git a/tests/util.py b/tests/util.py index 897bc108..2318bab7 100644 --- a/tests/util.py +++ b/tests/util.py @@ -170,7 +170,7 @@ class Cfg(Namespace): ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner" ka.update(**{k: "no" for k in ex.split()}) - ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" + ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm" ka.update(**{k: [] for k in ex.split()}) ex = "exp_lg exp_md"