Compare commits

...

7 Commits

Author SHA1 Message Date
Mikkeli Matlock 77750bd985 chore: ignoring yaml/yml files 2026-08-26 15:55:52 +09:00
Mikkeli Matlock 0b757cbfbf docs: revise batch input requirement for YAML 2026-08-26 15:54:52 +09:00
Mikkeli Matlock 81de21e039 feat: replace TOML input with YAML 2026-08-26 15:54:41 +09:00
Mikkeli Matlock 68229d9d1f docs: specify TOML batch input 2026-08-26 15:49:18 +09:00
Mikkeli Matlock 6ccf4d3fa6 feat: add SVG QR CLI and TOML batch input 2026-08-26 15:49:06 +09:00
Mikkeli Matlock 580c796491 chore: updated reqs 2026-08-26 15:39:36 +09:00
Mikkeli Matlock ec4f20a5ed chore: modified README and added Reqs 2026-08-26 15:37:52 +09:00
6 changed files with 180 additions and 35 deletions
-3
View File
@@ -1,3 +0,0 @@
# artifacts
output/
*.svg
+12
View File
@@ -0,0 +1,12 @@
# artifacts
output/
*.svg
# python and uv
uv.lock
.venv/
__pycache__/
# input
*.yaml
*.yml
+11 -3
View File
@@ -2,10 +2,18 @@
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 --yaml values.yaml
```
## notice
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`.
`--yaml` accepts a flat mapping of output name to text, such as
`name: text`.
+22
View File
@@ -0,0 +1,22 @@
# Reqs
## python package managment
- Use `uv`
- Ignore `uv.lock` even if it's against the spirit; causes trouble when going cross-architecture
- app should be runnable with only `uv run svgqr <ARGS>` or `svgqr <ARGS>`
## cli options (args)
- **raw text mode**: `-t <TEXT>` or `-t "<TEXT>"` (for those including space/tabs); `-t` can be omitted as this is the default mode.
- **output path**: `-o PATH/TO/SVG`. Automatically append `.svg` suffix if absent. When whole option is absent, use input text but path-incompatible characters substituted with `_`, and filename capped at 32 chars.
- **batch mode**: `-i <FILE_OF_LINES_TO_CONVERT>` ; output automatically dumps generated svgs per line of input file under `output/`; SVG files named after input lines verbatim except that path-incompatible characters substituted with `_` (underbar), and filename capped at 32 chars. Disregard `-o`.
- **batch mode with `yaml`**: `--yaml <LINES>.yaml`; the input YAML should be of a flat structure:
```yaml
item0: text0
item1: text1
item2: text2 and there can be spaces
```
output files go under `output/` and are named `item0.svg`, `item1.svg`, etc.
Reject invalid YAML without generating output files.
## svg generation
- Generate convex paths, not weighted edges
+20
View File
@@ -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",
"PyYAML>=6.0",
]
[project.scripts]
svgqr = "qr:run"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
only-include = ["qr.py"]
+115 -29
View File
@@ -1,41 +1,127 @@
import qrcode
"""Command-line SVG QR code generator."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import re
from typing import Sequence
def main(input_text, output_filename):
# Add .png extension if not present
if not output_filename.endswith(".png"):
output_filename += ".png"
import qrcode
import yaml
# Create a QR code object
qr = qrcode.QRCode(
version=1,
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,
box_size=10,
border=0,
border=4,
)
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"
)
# Add the input data to the QR code object
qr.add_data(input_text)
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"
)
# Make the QR code
qr.make(fit=True)
# Create an image object to store the generated QR code
img = qr.make_image(fill_color="black", back_color="white")
def write_svg(text: str, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(svg_path(text), encoding="utf-8")
# Save the QR code image to a file
img.save(output_filename)
def with_svg_suffix(path: Path) -> Path:
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_yaml_items(input_path: Path) -> dict[str, str]:
try:
items = yaml.safe_load(input_path.read_text(encoding="utf-8")) or {}
except yaml.YAMLError as error:
raise ValueError(f"{input_path}: invalid YAML: {error}") from None
if not isinstance(items, dict) or not all(
isinstance(key, str) and isinstance(value, str) for key, value in items.items()
):
raise ValueError(f"{input_path}: YAML must be a flat mapping of text values")
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("--yaml", type=Path, help="Flat YAML 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.yaml is not None:
if args.input is not None and args.yaml is not None:
parser.error("--input and --yaml 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_yaml_items(args.yaml)
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__":
# Set up the argument parser
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)
run()