qrforge 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qrforge/__init__.py +51 -0
- qrforge/__main__.py +6 -0
- qrforge/api.py +338 -0
- qrforge/bits.py +39 -0
- qrforge/cli.py +92 -0
- qrforge/color.py +235 -0
- qrforge/encoder.py +279 -0
- qrforge/exceptions.py +19 -0
- qrforge/gf.py +218 -0
- qrforge/gradient.py +270 -0
- qrforge/image.py +405 -0
- qrforge/matrix.py +363 -0
- qrforge/presets.py +97 -0
- qrforge/render/__init__.py +1 -0
- qrforge/render/ansi.py +75 -0
- qrforge/render/pdf.py +119 -0
- qrforge/render/raster.py +421 -0
- qrforge/render/svg.py +312 -0
- qrforge/shaders.py +828 -0
- qrforge/shapes.py +234 -0
- qrforge/style.py +185 -0
- qrforge/tables.py +138 -0
- qrforge/verify.py +210 -0
- qrforge-1.0.0.dist-info/METADATA +174 -0
- qrforge-1.0.0.dist-info/RECORD +29 -0
- qrforge-1.0.0.dist-info/WHEEL +5 -0
- qrforge-1.0.0.dist-info/entry_points.txt +2 -0
- qrforge-1.0.0.dist-info/licenses/LICENSE.md +7 -0
- qrforge-1.0.0.dist-info/top_level.txt +1 -0
qrforge/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""qrforge -- generate and style QR codes with no dependencies.
|
|
2
|
+
|
|
3
|
+
import qrforge
|
|
4
|
+
|
|
5
|
+
qrforge.save("https://example.com", "hello.png", "neon", scale=12)
|
|
6
|
+
|
|
7
|
+
qr = qrforge.QR("hello", ec="h")
|
|
8
|
+
qr.use_style(module_shape="dot", fg=["#ff0080", "#7928ca"], logo="logo.png")
|
|
9
|
+
qr.save("hello.svg")
|
|
10
|
+
|
|
11
|
+
Everything (encoding, Reed-Solomon, rasterisation, the PNG codec) is written in
|
|
12
|
+
pure Python against the standard library. Pillow is used only when you ask for
|
|
13
|
+
a non-PNG format, a PIL image, or a non-PNG logo.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .api import Artwork, QR, QRCode, make, png, save, svg, terminal
|
|
17
|
+
from .color import Color
|
|
18
|
+
from .exceptions import DataTooLong, QRForgeError, StyleError, UnsupportedFeature
|
|
19
|
+
from .gradient import Gradient, SolidFill
|
|
20
|
+
from .image import Pixmap
|
|
21
|
+
from .matrix import QRMatrix, build as build_matrix
|
|
22
|
+
from .shaders import Shader, expression, shader
|
|
23
|
+
from .shapes import Shape, custom as shape, register as register_shape
|
|
24
|
+
from .style import Style
|
|
25
|
+
|
|
26
|
+
__version__ = "1.0.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"QR", "QRCode", "Artwork", "make", "save", "svg", "png", "terminal",
|
|
30
|
+
"Style", "Color", "Gradient", "SolidFill", "Pixmap", "QRMatrix", "build_matrix",
|
|
31
|
+
"Shader", "shader", "expression", "Shape", "shape", "register_shape",
|
|
32
|
+
"presets", "shapes", "shaders", "verify",
|
|
33
|
+
"QRForgeError", "DataTooLong", "StyleError", "UnsupportedFeature",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name):
|
|
39
|
+
"""Lazily expose the heavier submodules (``qrforge.presets`` etc.)."""
|
|
40
|
+
if name in ("presets", "shapes", "shaders", "verify", "matrix", "encoder", "tables", "gf"):
|
|
41
|
+
from importlib import import_module
|
|
42
|
+
module = import_module(f".{name}", __name__)
|
|
43
|
+
globals()[name] = module
|
|
44
|
+
return module
|
|
45
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def preset(name: str) -> Style:
|
|
49
|
+
"""Shortcut for ``qrforge.presets.get(name)``."""
|
|
50
|
+
from .presets import get
|
|
51
|
+
return get(name)
|
qrforge/__main__.py
ADDED
qrforge/api.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""The public API: :class:`QR`, :class:`Artwork` and the shortcut functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from . import matrix as matrix_mod
|
|
8
|
+
from .exceptions import QRForgeError, UnsupportedFeature
|
|
9
|
+
from .image import Pixmap
|
|
10
|
+
from .style import Style, parse as parse_style
|
|
11
|
+
|
|
12
|
+
__all__ = ["QR", "QRCode", "Artwork", "make", "save", "svg", "png", "terminal"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Artwork:
|
|
16
|
+
"""A rendered symbol: either vector markup or a pixel buffer."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, kind: str, payload: Any, style: Style, matrix) -> None:
|
|
19
|
+
self.kind = kind # "svg" | "pixmap" | "text" | "pdf" | "bytes"
|
|
20
|
+
self.payload = payload
|
|
21
|
+
self.style = style
|
|
22
|
+
self.matrix = matrix
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ access
|
|
25
|
+
@property
|
|
26
|
+
def pixmap(self) -> Pixmap:
|
|
27
|
+
if self.kind != "pixmap":
|
|
28
|
+
raise QRForgeError(f"this artwork is {self.kind}, not a pixel buffer")
|
|
29
|
+
return self.payload
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def text(self) -> str:
|
|
33
|
+
if self.kind not in ("svg", "text"):
|
|
34
|
+
raise QRForgeError(f"this artwork is {self.kind}, not text")
|
|
35
|
+
return self.payload
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def width(self) -> int:
|
|
39
|
+
return self.pixmap.width if self.kind == "pixmap" else len(self.payload)
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return self.payload if isinstance(self.payload, str) else repr(self.payload)
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
if self.kind == "pixmap":
|
|
46
|
+
return f"<Artwork pixmap {self.payload.width}x{self.payload.height}>"
|
|
47
|
+
return f"<Artwork {self.kind} {len(self.payload)} bytes>"
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------ output
|
|
50
|
+
def save(self, path: str) -> str:
|
|
51
|
+
"""Write the artwork to ``path``; the format follows the extension."""
|
|
52
|
+
suffix = path.lower().rsplit(".", 1)[-1] if "." in path else "png"
|
|
53
|
+
if self.kind == "pixmap":
|
|
54
|
+
if suffix in ("png",):
|
|
55
|
+
with open(path, "wb") as handle:
|
|
56
|
+
handle.write(self.payload.to_png())
|
|
57
|
+
elif suffix in ("jpg", "jpeg", "webp", "bmp", "gif", "tiff"):
|
|
58
|
+
self.payload.save(path) # needs Pillow
|
|
59
|
+
elif suffix in ("svg",):
|
|
60
|
+
raise QRForgeError("cannot write a raster artwork as SVG; call .svg() instead")
|
|
61
|
+
else:
|
|
62
|
+
self.payload.save(path)
|
|
63
|
+
return path
|
|
64
|
+
data = self.payload
|
|
65
|
+
if isinstance(data, str):
|
|
66
|
+
data = data.encode("utf-8")
|
|
67
|
+
with open(path, "wb") as handle:
|
|
68
|
+
handle.write(data)
|
|
69
|
+
return path
|
|
70
|
+
|
|
71
|
+
def to_bytes(self) -> bytes:
|
|
72
|
+
if self.kind == "pixmap":
|
|
73
|
+
return self.payload.to_png()
|
|
74
|
+
data = self.payload
|
|
75
|
+
return data.encode("utf-8") if isinstance(data, str) else bytes(data)
|
|
76
|
+
|
|
77
|
+
def to_pil(self):
|
|
78
|
+
return self.pixmap.to_pil()
|
|
79
|
+
|
|
80
|
+
def to_data_uri(self) -> str:
|
|
81
|
+
import base64
|
|
82
|
+
if self.kind == "svg":
|
|
83
|
+
return "data:image/svg+xml;base64," + base64.b64encode(self.to_bytes()).decode()
|
|
84
|
+
return "data:image/png;base64," + base64.b64encode(self.to_bytes()).decode()
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------- notebook magic
|
|
87
|
+
def _repr_png_(self): # pragma: no cover - Jupyter
|
|
88
|
+
return self.to_bytes() if self.kind == "pixmap" else None
|
|
89
|
+
|
|
90
|
+
def _repr_svg_(self): # pragma: no cover - Jupyter
|
|
91
|
+
return self.payload if self.kind == "svg" else None
|
|
92
|
+
|
|
93
|
+
def _repr_html_(self): # pragma: no cover - Jupyter
|
|
94
|
+
if self.kind == "svg":
|
|
95
|
+
return self.payload
|
|
96
|
+
return f'<img src="{self.to_data_uri()}"/>'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class QR:
|
|
100
|
+
"""A QR code with its rendering style.
|
|
101
|
+
|
|
102
|
+
::
|
|
103
|
+
|
|
104
|
+
qr = qrforge.QR("https://example.com", ec="h")
|
|
105
|
+
qr.style(module_shape="dot", fg=["#ff0080", "#7928ca"], logo="logo.png")
|
|
106
|
+
qr.save("poster.png", scale=16)
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, data: Any, *, ec: str = "M", version: Optional[int] = None,
|
|
110
|
+
mask: Optional[int] = None, mode: str = "auto",
|
|
111
|
+
style: Any = None, **style_options: Any) -> None:
|
|
112
|
+
self.data = data
|
|
113
|
+
self.ec = ec.upper()
|
|
114
|
+
self.version = version
|
|
115
|
+
self.mask = mask
|
|
116
|
+
self.mode = mode
|
|
117
|
+
self._matrix = None
|
|
118
|
+
self._style = parse_style(style, **style_options)
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------- encoding
|
|
121
|
+
@property
|
|
122
|
+
def matrix(self):
|
|
123
|
+
"""The encoded :class:`~qrforge.matrix.QRMatrix` (built on first use)."""
|
|
124
|
+
if self._matrix is None:
|
|
125
|
+
self._matrix = matrix_mod.build(self.data, self.ec, self.version, self.mask, self.mode)
|
|
126
|
+
return self._matrix
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def size(self) -> int:
|
|
130
|
+
return self.matrix.size
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def style(self) -> Style:
|
|
134
|
+
return self._style
|
|
135
|
+
|
|
136
|
+
def encode(self, data: Any = None, **options: Any) -> "QR":
|
|
137
|
+
"""Change the payload/encoding options and drop the cached matrix."""
|
|
138
|
+
if data is not None:
|
|
139
|
+
self.data = data
|
|
140
|
+
for key in ("ec", "version", "mask", "mode"):
|
|
141
|
+
if key in options:
|
|
142
|
+
setattr(self, key, options.pop(key))
|
|
143
|
+
if options:
|
|
144
|
+
raise QRForgeError(f"unknown encoding option(s): {', '.join(options)}")
|
|
145
|
+
self._matrix = None
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def styled(self, style: Any = None, **options: Any) -> "QR":
|
|
149
|
+
"""Return a copy of this code with a different style."""
|
|
150
|
+
clone = QR(self.data, ec=self.ec, version=self.version, mask=self.mask, mode=self.mode)
|
|
151
|
+
clone._matrix = self._matrix
|
|
152
|
+
clone._style = self._resolve(style, options)
|
|
153
|
+
return clone
|
|
154
|
+
|
|
155
|
+
def use_style(self, style: Any = None, **options: Any) -> "QR":
|
|
156
|
+
"""Change this code's style in place (chainable)."""
|
|
157
|
+
if isinstance(style, Style):
|
|
158
|
+
self._style = self._style.merged(style, **options)
|
|
159
|
+
elif isinstance(style, dict):
|
|
160
|
+
self._style = self._style.merged(**style, **options)
|
|
161
|
+
elif isinstance(style, str) and style:
|
|
162
|
+
from .presets import get as preset_get
|
|
163
|
+
self._style = preset_get(style).merged(self._style, **options)
|
|
164
|
+
elif options:
|
|
165
|
+
self._style = self._style.merged(**options)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
# ----------------------------------------------------------------- outputs
|
|
169
|
+
def render(self, style: Any = None, **options: Any) -> Artwork:
|
|
170
|
+
"""Render to a pixel buffer (PNG-ready)."""
|
|
171
|
+
return self._artwork("pixmap", style, options)
|
|
172
|
+
|
|
173
|
+
def svg(self, style: Any = None, **options: Any) -> str:
|
|
174
|
+
"""Render to an SVG document."""
|
|
175
|
+
return self._artwork("svg", style, options).payload
|
|
176
|
+
|
|
177
|
+
def png(self, style: Any = None, **options: Any) -> bytes:
|
|
178
|
+
"""Render and encode as PNG bytes."""
|
|
179
|
+
return self._artwork("pixmap", style, options).payload.to_png()
|
|
180
|
+
|
|
181
|
+
def pdf(self, style: Any = None, **options: Any) -> bytes:
|
|
182
|
+
"""Render to a single-page vector PDF."""
|
|
183
|
+
from .render.pdf import render
|
|
184
|
+
return render(self.matrix, self._resolve(style, options))
|
|
185
|
+
|
|
186
|
+
def terminal(self, style: Any = None, color: bool = True, **options: Any) -> str:
|
|
187
|
+
"""Render as ANSI half-block text for a terminal."""
|
|
188
|
+
from .render.ansi import render
|
|
189
|
+
return render(self.matrix, self._resolve(style, options), color=color)
|
|
190
|
+
|
|
191
|
+
def print(self, style: Any = None, **options: Any) -> "QR":
|
|
192
|
+
print(self.terminal(style, **options))
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def ascii(self, dark: str = "██", light: str = " ") -> str:
|
|
196
|
+
"""Plain text art of the symbol."""
|
|
197
|
+
return self.matrix.ascii(dark, light)
|
|
198
|
+
|
|
199
|
+
def to_pil(self, style: Any = None, **options: Any):
|
|
200
|
+
"""Render and return a Pillow ``Image`` (requires Pillow)."""
|
|
201
|
+
return self.render(style, **options).to_pil()
|
|
202
|
+
|
|
203
|
+
def save(self, path: str, style: Any = None, **options: Any) -> str:
|
|
204
|
+
"""Render and write to ``path``; format chosen by extension."""
|
|
205
|
+
suffix = path.lower().rsplit(".", 1)[-1] if "." in path else "png"
|
|
206
|
+
if suffix in ("svg", "html"):
|
|
207
|
+
artwork = self._artwork("svg", style, options)
|
|
208
|
+
payload = artwork.payload
|
|
209
|
+
if suffix == "html":
|
|
210
|
+
payload = _html_page(payload, str(self.data))
|
|
211
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
212
|
+
handle.write(payload)
|
|
213
|
+
return path
|
|
214
|
+
if suffix == "pdf":
|
|
215
|
+
with open(path, "wb") as handle:
|
|
216
|
+
handle.write(self.pdf(style, **options))
|
|
217
|
+
return path
|
|
218
|
+
if suffix in ("txt", "ansi"):
|
|
219
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
220
|
+
handle.write(self.terminal(style, color=suffix == "ansi", **options))
|
|
221
|
+
return path
|
|
222
|
+
return self.render(style, **options).save(path)
|
|
223
|
+
|
|
224
|
+
def show(self, style: Any = None, **options: Any) -> "QR": # pragma: no cover - interactive
|
|
225
|
+
"""Open the rendered symbol in the system image viewer."""
|
|
226
|
+
import os
|
|
227
|
+
import tempfile
|
|
228
|
+
path = os.path.join(tempfile.gettempdir(), "qrforge.png")
|
|
229
|
+
self.render(style, **options).save(path)
|
|
230
|
+
try:
|
|
231
|
+
import webbrowser
|
|
232
|
+
webbrowser.open(f"file://{path}")
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------- misc
|
|
238
|
+
def verify(self) -> str:
|
|
239
|
+
"""Decode the symbol again and return the text (round-trip check)."""
|
|
240
|
+
from .verify import decode
|
|
241
|
+
return decode([list(row) for row in self.matrix.modules]).text
|
|
242
|
+
|
|
243
|
+
def info(self) -> dict:
|
|
244
|
+
matrix = self.matrix
|
|
245
|
+
return {
|
|
246
|
+
"data": str(self.data) if not isinstance(self.data, (bytes, bytearray)) else "<bytes>",
|
|
247
|
+
"version": matrix.version,
|
|
248
|
+
"level": matrix.level,
|
|
249
|
+
"mask": matrix.mask,
|
|
250
|
+
"size": matrix.size,
|
|
251
|
+
"modules": matrix.size ** 2,
|
|
252
|
+
"codewords": matrix.total_codewords(),
|
|
253
|
+
"dark_ratio": round(matrix.dark_ratio(), 4),
|
|
254
|
+
"penalty": matrix.penalty_score,
|
|
255
|
+
"segments": [(s.mode, len(s.text)) for s in matrix.segments],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def _resolve(self, style: Any, options: dict) -> Style:
|
|
259
|
+
if style is None:
|
|
260
|
+
return self._style.merged(**options) if options else self._style
|
|
261
|
+
if isinstance(style, Style):
|
|
262
|
+
return self._style.merged(style, **options)
|
|
263
|
+
if isinstance(style, dict):
|
|
264
|
+
return self._style.merged(**style, **options)
|
|
265
|
+
if isinstance(style, str):
|
|
266
|
+
from .presets import get as preset_get
|
|
267
|
+
return preset_get(style).merged(**options)
|
|
268
|
+
raise QRForgeError(f"cannot use {style!r} as a style")
|
|
269
|
+
|
|
270
|
+
def _artwork(self, kind: str, style: Any, options: dict) -> Artwork:
|
|
271
|
+
resolved = self._resolve(style, options)
|
|
272
|
+
matrix = self.matrix
|
|
273
|
+
if kind == "svg":
|
|
274
|
+
from .render.svg import render
|
|
275
|
+
return Artwork("svg", render(matrix, resolved), resolved, matrix)
|
|
276
|
+
from .render.raster import render as raster_render
|
|
277
|
+
return Artwork("pixmap", raster_render(matrix, resolved), resolved, matrix)
|
|
278
|
+
|
|
279
|
+
def __repr__(self) -> str:
|
|
280
|
+
return f"<QR {str(self.data)[:28]!r} ec={self.ec} style={self._style!r}>"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
QRCode = QR # friendly alias
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _html_page(svg: str, title: str) -> str:
|
|
287
|
+
return ("<!doctype html><meta charset='utf-8'>"
|
|
288
|
+
f"<title>{title[:60]}</title>"
|
|
289
|
+
"<style>body{margin:0;display:grid;place-items:center;min-height:100vh;"
|
|
290
|
+
"background:#111;font:14px system-ui}"
|
|
291
|
+
"main{padding:32px;background:#fff;border-radius:16px;box-shadow:0 20px 60px #0008}"
|
|
292
|
+
"</style><main>" + svg + "</main>")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------------- shortcuts
|
|
296
|
+
def make(data: Any, style: Any = None, **options: Any) -> Artwork:
|
|
297
|
+
"""Render ``data`` in one call::
|
|
298
|
+
|
|
299
|
+
qrforge.make("hello", "dots", fg="#123").save("hello.png")
|
|
300
|
+
"""
|
|
301
|
+
preset = style if isinstance(style, str) else None
|
|
302
|
+
extra = dict(style) if isinstance(style, dict) else {}
|
|
303
|
+
qr = QR(data)
|
|
304
|
+
qr._style = parse_style(preset, **{**extra, **options})
|
|
305
|
+
return qr.render()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def save(data: Any, path: str, style: Any = None, **options: Any) -> str:
|
|
309
|
+
"""Render ``data`` straight to ``path``."""
|
|
310
|
+
preset = style if isinstance(style, str) else None
|
|
311
|
+
extra = dict(style) if isinstance(style, dict) else {}
|
|
312
|
+
qr = QR(data)
|
|
313
|
+
qr._style = parse_style(preset, **{**extra, **options})
|
|
314
|
+
return qr.save(path)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def svg(data: Any, style: Any = None, **options: Any) -> str:
|
|
318
|
+
preset = style if isinstance(style, str) else None
|
|
319
|
+
extra = dict(style) if isinstance(style, dict) else {}
|
|
320
|
+
qr = QR(data)
|
|
321
|
+
qr._style = parse_style(preset, **{**extra, **options})
|
|
322
|
+
return qr.svg()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def png(data: Any, style: Any = None, **options: Any) -> bytes:
|
|
326
|
+
preset = style if isinstance(style, str) else None
|
|
327
|
+
extra = dict(style) if isinstance(style, dict) else {}
|
|
328
|
+
qr = QR(data)
|
|
329
|
+
qr._style = parse_style(preset, **{**extra, **options})
|
|
330
|
+
return qr.png()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def terminal(data: Any, style: Any = None, **options: Any) -> str:
|
|
334
|
+
preset = style if isinstance(style, str) else None
|
|
335
|
+
extra = dict(style) if isinstance(style, dict) else {}
|
|
336
|
+
qr = QR(data)
|
|
337
|
+
qr._style = parse_style(preset, **{**extra, **options})
|
|
338
|
+
return qr.terminal()
|
qrforge/bits.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""A tiny MSB-first bit buffer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BitBuffer:
|
|
9
|
+
__slots__ = ("_data", "length")
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._data = bytearray()
|
|
13
|
+
self.length = 0
|
|
14
|
+
|
|
15
|
+
def append(self, value: int, nbits: int) -> None:
|
|
16
|
+
if nbits < 0 or value >> nbits:
|
|
17
|
+
raise ValueError(f"cannot store {value} in {nbits} bits")
|
|
18
|
+
for i in range(nbits - 1, -1, -1):
|
|
19
|
+
bit = (value >> i) & 1
|
|
20
|
+
if not (self.length & 7):
|
|
21
|
+
self._data.append(0)
|
|
22
|
+
self._data[self.length >> 3] |= bit << (7 - (self.length & 7))
|
|
23
|
+
self.length += 1
|
|
24
|
+
|
|
25
|
+
def append_all(self, bits: Iterable[bool]) -> None:
|
|
26
|
+
for b in bits:
|
|
27
|
+
self.append(1 if b else 0, 1)
|
|
28
|
+
|
|
29
|
+
def bit(self, index: int) -> int:
|
|
30
|
+
return (self._data[index >> 3] >> (7 - (index & 7))) & 1
|
|
31
|
+
|
|
32
|
+
def to_bytes(self) -> bytes:
|
|
33
|
+
return bytes(self._data)
|
|
34
|
+
|
|
35
|
+
def __len__(self) -> int:
|
|
36
|
+
return self.length
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
39
|
+
return "".join(str(self.bit(i)) for i in range(min(self.length, 64)))
|
qrforge/cli.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Command line interface: ``python -m qrforge``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import presets, shaders, shapes
|
|
9
|
+
from .api import QR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="qrforge",
|
|
15
|
+
description="Generate and style QR codes from the command line.")
|
|
16
|
+
parser.add_argument("data", nargs="?", help="the text to encode")
|
|
17
|
+
parser.add_argument("-o", "--output", help="output file (.png/.svg/.pdf/.txt/.html)")
|
|
18
|
+
parser.add_argument("-p", "--preset", help="a named style; see --list-presets")
|
|
19
|
+
parser.add_argument("-e", "--ec", default="M", choices="LMQH", help="error correction")
|
|
20
|
+
parser.add_argument("-v", "--version", type=int, help="force QR version (1-40)")
|
|
21
|
+
parser.add_argument("-m", "--mask", type=int, help="force mask (0-7)")
|
|
22
|
+
parser.add_argument("-s", "--scale", type=int, default=10, help="pixels per module")
|
|
23
|
+
parser.add_argument("--shape", help="module shape; see --list-shapes")
|
|
24
|
+
parser.add_argument("--fg", help="foreground colour or comma separated gradient")
|
|
25
|
+
parser.add_argument("--bg", help="background colour (or 'none' for transparent)")
|
|
26
|
+
parser.add_argument("--logo", help="path to a logo image")
|
|
27
|
+
parser.add_argument("--shader", action="append", help="shader, repeatable; see --list-shaders")
|
|
28
|
+
parser.add_argument("--seed", type=int, help="seed for randomised effects")
|
|
29
|
+
parser.add_argument("--terminal", action="store_true", help="print to the terminal")
|
|
30
|
+
parser.add_argument("--info", action="store_true", help="show encoding info")
|
|
31
|
+
parser.add_argument("--list-presets", action="store_true")
|
|
32
|
+
parser.add_argument("--list-shapes", action="store_true")
|
|
33
|
+
parser.add_argument("--list-shaders", action="store_true")
|
|
34
|
+
parser.add_argument("--version-info", action="store_true", help="print the library version")
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv=None) -> int:
|
|
39
|
+
args = build_parser().parse_args(argv)
|
|
40
|
+
|
|
41
|
+
if args.list_presets:
|
|
42
|
+
print(", ".join(presets.names()))
|
|
43
|
+
return 0
|
|
44
|
+
if args.list_shapes:
|
|
45
|
+
print(", ".join(shapes.names()))
|
|
46
|
+
return 0
|
|
47
|
+
if args.list_shaders:
|
|
48
|
+
print(", ".join(shaders.names()))
|
|
49
|
+
return 0
|
|
50
|
+
if args.version_info:
|
|
51
|
+
from . import __version__
|
|
52
|
+
print(__version__)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
if not args.data:
|
|
56
|
+
build_parser().print_help()
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
options = {"ec": args.ec, "version": args.version, "mask": args.mask}
|
|
60
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
61
|
+
qr = QR(args.data, **options)
|
|
62
|
+
|
|
63
|
+
style = {}
|
|
64
|
+
if args.shape:
|
|
65
|
+
style["module_shape"] = args.shape.split(",") if "," in args.shape else args.shape
|
|
66
|
+
if args.fg:
|
|
67
|
+
style["fg"] = args.fg.split(",") if "," in args.fg else args.fg
|
|
68
|
+
if args.bg:
|
|
69
|
+
style["bg"] = None if args.bg.lower() in ("none", "transparent") else args.bg
|
|
70
|
+
if args.logo:
|
|
71
|
+
style["logo"] = args.logo
|
|
72
|
+
if args.shader:
|
|
73
|
+
style["shaders"] = tuple(args.shader)
|
|
74
|
+
if args.seed is not None:
|
|
75
|
+
style["seed"] = args.seed
|
|
76
|
+
|
|
77
|
+
if args.info:
|
|
78
|
+
for key, value in qr.info().items():
|
|
79
|
+
print(f"{key}: {value}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if args.terminal or not args.output:
|
|
83
|
+
print(qr.terminal(args.preset, **style))
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
qr.save(args.output, args.preset, scale=args.scale, **style)
|
|
87
|
+
print(f"saved {args.output}")
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__": # pragma: no cover
|
|
92
|
+
sys.exit(main())
|