feat: add SVG QR CLI and TOML batch input
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# artifacts
|
||||||
|
output/
|
||||||
|
*.svg
|
||||||
|
uv.lock
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
Generates SVG QR codes from any text input.
|
Generates SVG QR codes from any text input.
|
||||||
|
|
||||||
## usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
python qr.py <TEXT> -o <OUTPUT_PATH>
|
uv run svgqr "Text to encode"
|
||||||
```
|
uv run svgqr -t "Text to encode" -o path/to/code.svg
|
||||||
|
uv run svgqr -i values.txt
|
||||||
|
uv run svgqr --toml values.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Text can be passed positionally or with `-t`. Without `-o`, the program uses a
|
||||||
|
safe, truncated form of the text as the SVG filename. Batch mode writes SVGs to
|
||||||
|
`output/` and ignores `-o`.
|
||||||
|
|
||||||
|
`--toml` accepts a flat mapping of output name to text. Standard TOML uses
|
||||||
|
`name = "text"`; the documented `name: text` form is also accepted.
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[project]
|
||||||
|
name = "svg-qr-gen"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Generate SVG QR codes from text."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"qrcode>=8.0",
|
||||||
|
"tomli>=2.0; python_version < '3.11'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
svgqr = "qr:run"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
only-include = ["qr.py"]
|
||||||
@@ -1,41 +1,139 @@
|
|||||||
import qrcode
|
"""Command-line SVG QR code generator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
def main(input_text, output_filename):
|
import qrcode
|
||||||
# Add .png extension if not present
|
|
||||||
if not output_filename.endswith(".png"):
|
|
||||||
output_filename += ".png"
|
|
||||||
|
|
||||||
# Create a QR code object
|
try:
|
||||||
qr = qrcode.QRCode(
|
import tomllib
|
||||||
version=1,
|
except ModuleNotFoundError:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
|
||||||
|
SVG_SUFFIX = ".svg"
|
||||||
|
MAX_FILENAME_LENGTH = 32
|
||||||
|
INVALID_FILENAME_CHARACTERS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||||
|
|
||||||
|
|
||||||
|
def svg_path(text: str) -> str:
|
||||||
|
"""Build an SVG containing the QR code as filled convex module paths."""
|
||||||
|
code = qrcode.QRCode(
|
||||||
|
version=None,
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
box_size=10,
|
border=4,
|
||||||
border=0,
|
)
|
||||||
|
code.add_data(text)
|
||||||
|
code.make(fit=True)
|
||||||
|
|
||||||
|
matrix = code.get_matrix()
|
||||||
|
size = len(matrix)
|
||||||
|
modules = []
|
||||||
|
for row_index, row in enumerate(matrix):
|
||||||
|
for column_index, is_dark in enumerate(row):
|
||||||
|
if is_dark:
|
||||||
|
modules.append(
|
||||||
|
f"M{column_index} {row_index}H{column_index + 1}"
|
||||||
|
f"V{row_index + 1}H{column_index}Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
path_data = "".join(modules)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" '
|
||||||
|
f'width="{size}" height="{size}" role="img">\n'
|
||||||
|
f' <path fill="#000" d="{path_data}"/>\n'
|
||||||
|
"</svg>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the input data to the QR code object
|
|
||||||
qr.add_data(input_text)
|
|
||||||
|
|
||||||
# Make the QR code
|
def write_svg(text: str, output_path: Path) -> None:
|
||||||
qr.make(fit=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(svg_path(text), encoding="utf-8")
|
||||||
|
|
||||||
# Create an image object to store the generated QR code
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
|
|
||||||
# Save the QR code image to a file
|
def with_svg_suffix(path: Path) -> Path:
|
||||||
img.save(output_filename)
|
if path.suffix.lower() == SVG_SUFFIX:
|
||||||
|
return path
|
||||||
|
return path.with_name(f"{path.name}{SVG_SUFFIX}")
|
||||||
|
|
||||||
|
|
||||||
|
def automatic_output_path(text: str, directory: Path = Path(".")) -> Path:
|
||||||
|
filename = INVALID_FILENAME_CHARACTERS.sub("_", text).rstrip(". ")
|
||||||
|
if filename in {"", ".", ".."}:
|
||||||
|
filename = "_"
|
||||||
|
|
||||||
|
stem_length = MAX_FILENAME_LENGTH - len(SVG_SUFFIX)
|
||||||
|
return directory / f"{filename[:stem_length]}{SVG_SUFFIX}"
|
||||||
|
|
||||||
|
|
||||||
|
def read_toml_items(input_path: Path) -> dict[str, str]:
|
||||||
|
content = input_path.read_text(encoding="utf-8")
|
||||||
|
try:
|
||||||
|
items = tomllib.loads(content)
|
||||||
|
except tomllib.TOMLDecodeError:
|
||||||
|
items = {}
|
||||||
|
for line_number, line in enumerate(content.splitlines(), start=1):
|
||||||
|
if not line.strip() or line.lstrip().startswith("#"):
|
||||||
|
continue
|
||||||
|
key, separator, value = line.partition(":")
|
||||||
|
if not separator or not key.strip():
|
||||||
|
raise ValueError(
|
||||||
|
f"{input_path}:{line_number}: expected a flat TOML item"
|
||||||
|
) from None
|
||||||
|
items[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
if not all(isinstance(key, str) and isinstance(value, str) for key, value in items.items()):
|
||||||
|
raise ValueError(f"{input_path}: TOML values must be top-level text strings")
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Generate SVG QR codes from text.")
|
||||||
|
parser.add_argument("text", nargs="?", help="Text to encode (the default input mode).")
|
||||||
|
parser.add_argument("-t", "--text", dest="option_text", help="Text to encode.")
|
||||||
|
parser.add_argument("-o", "--output", type=Path, help="Path for the generated SVG.")
|
||||||
|
parser.add_argument("-i", "--input", type=Path, help="File containing one QR value per line.")
|
||||||
|
parser.add_argument("--toml", type=Path, help="Flat TOML file containing output names and text.")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run(arguments: Sequence[str] | None = None) -> None:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(arguments)
|
||||||
|
|
||||||
|
if args.input is not None or args.toml is not None:
|
||||||
|
if args.input is not None and args.toml is not None:
|
||||||
|
parser.error("--input and --toml cannot be used together")
|
||||||
|
if args.text is not None or args.option_text is not None:
|
||||||
|
parser.error("text input cannot be used with batch input")
|
||||||
|
|
||||||
|
if args.input is not None:
|
||||||
|
for line in args.input.read_text(encoding="utf-8").splitlines():
|
||||||
|
write_svg(line, automatic_output_path(line, Path("output")))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
items = read_toml_items(args.toml)
|
||||||
|
except ValueError as error:
|
||||||
|
parser.error(str(error))
|
||||||
|
for name, text in items.items():
|
||||||
|
write_svg(text, automatic_output_path(name, Path("output")))
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.text is not None and args.option_text is not None:
|
||||||
|
parser.error("provide text either positionally or with --text, not both")
|
||||||
|
|
||||||
|
text = args.option_text if args.option_text is not None else args.text
|
||||||
|
if text is None:
|
||||||
|
parser.error("text is required unless --input is provided")
|
||||||
|
|
||||||
|
output_path = with_svg_suffix(args.output) if args.output else automatic_output_path(text)
|
||||||
|
write_svg(text, output_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Set up the argument parser
|
run()
|
||||||
parser = argparse.ArgumentParser(description="Generate a QR code from input text.")
|
|
||||||
parser.add_argument("-i", "--input", required=True, help="Text to be embedded in the QR code.")
|
|
||||||
parser.add_argument("-o", "--output", required=True, help="Filename for the output image.")
|
|
||||||
|
|
||||||
# Parse the command-line arguments
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Call the main function with the provided arguments
|
|
||||||
main(args.input, args.output)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user