init
This commit is contained in:
commit
e6ec79ba64
209
README.md
Normal file
209
README.md
Normal file
@ -0,0 +1,209 @@
|
||||
# QR-ASCII-QR
|
||||
|
||||
一个强大且灵活的二维码生成工具,能够创建独特的"字符二维码"——在传统二维码的基础上叠加自定义字符,既保持完整的扫描功能,又具有独特的视觉效果。
|
||||
|
||||
[](https://python.org)
|
||||
[](LICENSE)
|
||||
|
||||
## ✨ 主要特色
|
||||
|
||||
### 🎯 多种渲染模式
|
||||
- **纯黑块模式 (solid)**: 传统的黑白二维码
|
||||
- **纯文字模式 (text)**: 仅使用字符构成的二维码(定位点保持黑块)
|
||||
- **组合模式 (combined)**: 黑色背景上叠加字符,视觉效果最佳
|
||||
|
||||
### 🔧 强大的自定义功能
|
||||
- **自定义字符集**: 支持数字、字母、汉字等任意字符组合
|
||||
- **多种输出格式**: PNG 位图和 SVG 矢量图
|
||||
- **字体自定义**: 支持 TrueType 字体文件
|
||||
- **Logo 嵌入**: 可在二维码中心添加 Logo(PNG 模式)
|
||||
- **高纠错级别**: 默认 H 级(30% 容错),确保可扫描性
|
||||
|
||||
### 🚀 批量生成
|
||||
- **三合一模式**: 一次命令生成三种不同风格的二维码
|
||||
- **智能定位点处理**: 自动识别并优化定位点区域的显示效果
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 环境要求
|
||||
- Python 3.7 或更高版本
|
||||
- 支持 Windows、macOS、Linux
|
||||
|
||||
### 快速安装
|
||||
```bash
|
||||
# 2. 创建虚拟环境(推荐)
|
||||
python -m venv venv
|
||||
|
||||
# 3. 激活虚拟环境
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
# macOS/Linux:
|
||||
source venv/bin/activate
|
||||
|
||||
# 4. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 依赖包说明
|
||||
- `qrcode>=7.4.2`: 二维码生成核心库
|
||||
- `pillow>=10.0.0`: 图像处理库,用于 PNG 渲染
|
||||
- `svgwrite>=1.4.3`: SVG 文件生成库
|
||||
|
||||
## 🎮 快速开始
|
||||
|
||||
### 基础使用
|
||||
```bash
|
||||
# 只生成字符二维码
|
||||
python main.py -d "网站URL" -o "文件名" -c "hash值" --text-only
|
||||
|
||||
# 生成三种二维码
|
||||
python main.py -d "网站URL" -o "文件名" -c "hash值" --three-types
|
||||
|
||||
|
||||
### 高级使用
|
||||
```bash
|
||||
# 使用自定义字体和 Logo
|
||||
python main.py -d "Branded QR" -o branded.png \
|
||||
--font ./SIMHEI.TTF \
|
||||
--logo ./logo.png \
|
||||
-s 30
|
||||
|
||||
# 指定纠错级别
|
||||
python main.py -d "High Error Correction" -o high_ec.png -e H
|
||||
|
||||
# 大尺寸二维码
|
||||
python main.py -d "Large QR" -o large.png -s 50
|
||||
```
|
||||
|
||||
## 📖 详细参数说明
|
||||
|
||||
### 命令行参数
|
||||
|
||||
| 参数 | 长格式 | 必需 | 默认值 | 说明 |
|
||||
|------|--------|------|--------|------|
|
||||
| `-d` | `--data` | ✅ | - | 要编码的数据(文本、URL、任意字符串) |
|
||||
| `-o` | `--out` | ✅ | - | 输出文件路径,支持 .png 和 .svg 扩展名 |
|
||||
| `-s` | `--cell-size` | ❌ | 20 | 单元格像素大小(仅 PNG 有效) |
|
||||
| `-c` | `--charset` | ❌ | `0-9A-Z` | 用作填充的字符集合 |
|
||||
| `-e` | `--ec-level` | ❌ | H | 纠错级别:L(7%) / M(15%) / Q(25%) / H(30%) |
|
||||
| | `--font` | ❌ | 自动检测 | TrueType 字体文件路径(仅 PNG 有效) |
|
||||
| | `--logo` | ❌ | - | Logo 图片路径(仅 PNG 有效) |
|
||||
| | `--svg` | ❌ | false | 强制输出 SVG 格式 |
|
||||
| | `--three-types` | ❌ | false | 生成三种渲染模式的二维码 |
|
||||
|
||||
|
||||
|
||||
## 🔧 API 使用
|
||||
|
||||
### 基本 API
|
||||
```python
|
||||
from qr_ascii_qr import generate_qr_image, generate_qr_svg
|
||||
|
||||
# 生成 PNG 图片
|
||||
img = generate_qr_image(
|
||||
data="Hello API",
|
||||
cell_size=25,
|
||||
charset="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
ec_level="H"
|
||||
)
|
||||
img.show() # 显示图片
|
||||
img.save("api_example.png") # 保存图片
|
||||
|
||||
# 生成 SVG 文件
|
||||
svg_path = generate_qr_svg(
|
||||
data="SVG Example",
|
||||
out_path="api_example.svg",
|
||||
cell_size=20,
|
||||
charset="★☆♦♣♠♥♪♫"
|
||||
)
|
||||
```
|
||||
|
||||
### 高级 API
|
||||
```python
|
||||
from qr_ascii_qr import generate_matrix, render_to_png, render_to_svg
|
||||
|
||||
# 生成矩阵
|
||||
matrix = generate_matrix("Advanced Usage", ec_level="H")
|
||||
|
||||
# 渲染为 PNG(支持多种模式)
|
||||
render_to_png(
|
||||
matrix=matrix,
|
||||
out_path="advanced.png",
|
||||
cell_size=30,
|
||||
charset="0123456789ABCDEF",
|
||||
font_path="./fonts/custom.ttf",
|
||||
logo_path="./logo.png",
|
||||
render_mode="combined" # 'solid', 'text', 'combined'
|
||||
)
|
||||
|
||||
# 渲染为 SVG
|
||||
render_to_svg(
|
||||
matrix=matrix,
|
||||
out_path="advanced.svg",
|
||||
cell_size=25,
|
||||
charset="♠♣♥♦",
|
||||
font_family="Arial"
|
||||
)
|
||||
```
|
||||
|
||||
## 🎨 渲染模式详解
|
||||
|
||||
### 1. 纯黑块模式 (solid)
|
||||
传统的黑白二维码,与标准二维码完全一致。
|
||||
|
||||
### 2. 纯文字模式 (text)
|
||||
- 定位点区域:保持黑色方块(确保扫描识别)
|
||||
- 数据区域:使用指定字符替代黑色方块
|
||||
- 背景:白色
|
||||
- 适用场景:需要清晰可读字符的场合
|
||||
|
||||
### 3. 组合模式 (combined)
|
||||
- 保持原有黑色方块
|
||||
- 在黑色背景上叠加字符
|
||||
- 字符颜色:黑色(在实际应用中可以调整为其他颜色)
|
||||
- 视觉效果:最佳的艺术效果与扫描性能平衡
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
qr_ascii_qr/
|
||||
├── qr_ascii_qr/ # 核心包
|
||||
│ ├── __init__.py # 包导出接口
|
||||
│ └── generator.py # 二维码生成核心逻辑
|
||||
├── main.py # 命令行入口
|
||||
├── requirements.txt # 项目依赖
|
||||
├── SIMHEI.TTF # 默认中文字体
|
||||
├── logo.png # 示例 Logo
|
||||
├── README.md # 项目文档
|
||||
└── venv/ # 虚拟环境(安装后生成)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: 生成的二维码无法扫描?**
|
||||
A: 检查以下几点:
|
||||
- 确保使用足够高的纠错级别(推荐 H 级)
|
||||
- 验证单元格大小是否足够大(建议 ≥ 20 像素)
|
||||
- 在组合模式下,确保字符颜色与背景有足够对比度
|
||||
|
||||
**Q: 字符显示不正确或乱码?**
|
||||
A:
|
||||
- 确保使用支持目标字符的字体文件
|
||||
- 检查字符集编码是否正确(推荐使用 UTF-8)
|
||||
- 验证字体文件路径是否正确
|
||||
|
||||
**Q: 程序运行缓慢?**
|
||||
A:
|
||||
- 减小单元格大小以降低图片分辨率
|
||||
- 简化字符集,减少复杂字符的使用
|
||||
- 考虑使用 SVG 格式替代大尺寸 PNG
|
||||
|
||||
### 错误代码
|
||||
- `FileNotFoundError`: 字体文件或 Logo 文件路径错误
|
||||
- `UnicodeError`: 字符集包含不支持的字符
|
||||
- `PIL.UnidentifiedImageError`: Logo 文件格式不支持
|
||||
|
BIN
SIMHEI.TTF
Normal file
BIN
SIMHEI.TTF
Normal file
Binary file not shown.
103
main.py
Normal file
103
main.py
Normal file
@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python
|
||||
"""Command‑line interface for QR-ASCII-QR."""
|
||||
import argparse
|
||||
import os
|
||||
from qr_ascii_qr import generate_matrix, render_to_png, render_to_svg
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate an ASCII-character QR code.")
|
||||
parser.add_argument("-d", "--data", required=True, help="Data to encode")
|
||||
parser.add_argument("-o", "--out", required=True, help="Output file prefix (.png or .svg)")
|
||||
parser.add_argument("-s", "--cell-size", type=int, default=20, help="Pixel size of each cell (PNG only)")
|
||||
parser.add_argument("-c", "--charset", default="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", help="Characters to use")
|
||||
parser.add_argument("-e", "--ec-level", default="H", choices=list("LMQH"), help="Error correction level")
|
||||
parser.add_argument("--font", help="TrueType font path (PNG only)")
|
||||
parser.add_argument("--logo", help="Logo image path (PNG only)")
|
||||
parser.add_argument("--svg", action="store_true", help="Force SVG output")
|
||||
parser.add_argument("--three-types", action="store_true", help="Generate three types of QR codes")
|
||||
parser.add_argument("--text-only", action="store_true", help="Generate only text QR code")
|
||||
args = parser.parse_args()
|
||||
|
||||
matrix = generate_matrix(args.data, args.ec_level)
|
||||
|
||||
if args.text_only:
|
||||
# 只生成纯文字二维码
|
||||
ext = args.out.lower().split(".")[-1]
|
||||
if args.svg or ext == "svg":
|
||||
render_to_svg(matrix, args.out, cell_size=args.cell_size, charset=args.charset)
|
||||
else:
|
||||
render_to_png(
|
||||
matrix,
|
||||
args.out,
|
||||
cell_size=args.cell_size,
|
||||
charset=args.charset,
|
||||
font_path=args.font,
|
||||
logo_path=args.logo,
|
||||
render_mode="text"
|
||||
)
|
||||
print(f"已保存纯文字二维码: {args.out}")
|
||||
elif args.three_types:
|
||||
# 生成三种类型的图片
|
||||
base_name, ext = os.path.splitext(args.out)
|
||||
if args.svg or ext.lower() == ".svg":
|
||||
# SVG模式
|
||||
render_to_svg(matrix, f"{base_name}_combined{ext}", cell_size=args.cell_size, charset=args.charset)
|
||||
print(f"Saved {base_name}_combined{ext}")
|
||||
else:
|
||||
# PNG模式 - 生成三张图片
|
||||
# 1. 纯黑块二维码
|
||||
solid_file = f"{base_name}_solid{ext}"
|
||||
render_to_png(
|
||||
matrix,
|
||||
solid_file,
|
||||
cell_size=args.cell_size,
|
||||
charset=args.charset,
|
||||
font_path=args.font,
|
||||
logo_path=args.logo,
|
||||
render_mode="solid"
|
||||
)
|
||||
print(f"已保存纯黑块二维码: {solid_file}")
|
||||
|
||||
# 2. 纯文字二维码
|
||||
text_file = f"{base_name}_text{ext}"
|
||||
render_to_png(
|
||||
matrix,
|
||||
text_file,
|
||||
cell_size=args.cell_size,
|
||||
charset=args.charset,
|
||||
font_path=args.font,
|
||||
logo_path=args.logo,
|
||||
render_mode="text"
|
||||
)
|
||||
print(f"已保存纯文字二维码: {text_file}")
|
||||
|
||||
# 3. 结合二维码(黑色背景+文字)
|
||||
combined_file = f"{base_name}_combined{ext}"
|
||||
render_to_png(
|
||||
matrix,
|
||||
combined_file,
|
||||
cell_size=args.cell_size,
|
||||
charset=args.charset,
|
||||
font_path=args.font,
|
||||
logo_path=args.logo,
|
||||
render_mode="combined"
|
||||
)
|
||||
print(f"已保存结合二维码: {combined_file}")
|
||||
else:
|
||||
# 原有的单个文件生成逻辑
|
||||
ext = args.out.lower().split(".")[-1]
|
||||
if args.svg or ext == "svg":
|
||||
render_to_svg(matrix, args.out, cell_size=args.cell_size, charset=args.charset)
|
||||
else:
|
||||
render_to_png(
|
||||
matrix,
|
||||
args.out,
|
||||
cell_size=args.cell_size,
|
||||
charset=args.charset,
|
||||
font_path=args.font,
|
||||
logo_path=args.logo,
|
||||
)
|
||||
print(f"Saved {args.out}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
7
qr_ascii_qr/__init__.py
Normal file
7
qr_ascii_qr/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from .generator import (
|
||||
generate_matrix,
|
||||
render_to_png,
|
||||
render_to_svg,
|
||||
generate_qr_image,
|
||||
generate_qr_svg,
|
||||
)
|
BIN
qr_ascii_qr/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
qr_ascii_qr/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
qr_ascii_qr/__pycache__/generator.cpython-313.pyc
Normal file
BIN
qr_ascii_qr/__pycache__/generator.cpython-313.pyc
Normal file
Binary file not shown.
351
qr_ascii_qr/generator.py
Normal file
351
qr_ascii_qr/generator.py
Normal file
@ -0,0 +1,351 @@
|
||||
"""QR code rendered with alphanumeric characters."""
|
||||
import qrcode
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import svgwrite
|
||||
from typing import List
|
||||
import os
|
||||
|
||||
DEFAULT_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
def is_finder_pattern(x: int, y: int, matrix_size: int) -> bool:
|
||||
"""判断给定坐标是否在QR码的三个定位点区域内(包含分隔符的8x8模块)"""
|
||||
# 边框已被裁剪,定位点从(0,0)开始
|
||||
|
||||
# 左上角定位点区域 (0,0) 到 (7,7) - 包含7x7定位点和1像素分隔符
|
||||
if 0 <= x <= 7 and 0 <= y <= 7:
|
||||
return True
|
||||
|
||||
# 右上角定位点区域 (matrix_size-8,0) 到 (matrix_size-1,7)
|
||||
if (matrix_size - 8) <= x <= (matrix_size - 1) and 0 <= y <= 7:
|
||||
return True
|
||||
|
||||
# 左下角定位点区域 (0,matrix_size-8) 到 (7,matrix_size-1)
|
||||
if 0 <= x <= 7 and (matrix_size - 8) <= y <= (matrix_size - 1):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def generate_matrix(data: str, ec_level: str = "H") -> List[List[bool]]:
|
||||
"""Return boolean matrix of the QR code."""
|
||||
level_map = {
|
||||
"L": qrcode.constants.ERROR_CORRECT_L,
|
||||
"M": qrcode.constants.ERROR_CORRECT_M,
|
||||
"Q": qrcode.constants.ERROR_CORRECT_Q,
|
||||
"H": qrcode.constants.ERROR_CORRECT_H,
|
||||
}
|
||||
qr = qrcode.QRCode(error_correction=level_map.get(ec_level.upper(), qrcode.constants.ERROR_CORRECT_H))
|
||||
qr.add_data(data)
|
||||
qr.make()
|
||||
return qr.get_matrix()
|
||||
|
||||
def render_to_png(
|
||||
matrix,
|
||||
out_path: str,
|
||||
cell_size: int = 20,
|
||||
charset: str = DEFAULT_CHARSET,
|
||||
font_path: str | None = None,
|
||||
logo_path: str | None = None,
|
||||
render_mode: str = "combined", # 新增参数:'solid', 'text', 'combined'
|
||||
):
|
||||
# 去掉QR码的4像素边框
|
||||
border = 4
|
||||
original_size = len(matrix)
|
||||
|
||||
# 裁剪矩阵,去掉边框
|
||||
cropped_matrix = []
|
||||
for y in range(border, original_size - border):
|
||||
row = []
|
||||
for x in range(border, original_size - border):
|
||||
row.append(matrix[y][x])
|
||||
cropped_matrix.append(row)
|
||||
|
||||
matrix = cropped_matrix
|
||||
N = len(matrix)
|
||||
img_size = N * cell_size
|
||||
img = Image.new("RGBA", (img_size, img_size), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
|
||||
|
||||
# 设置字体,优先使用指定字体或项目中的字体
|
||||
font_size = int(cell_size * 0.7) # 基础字体大小
|
||||
font_size_large = int(cell_size * 0.9) # 第一个和最后一个字符的更大字体
|
||||
|
||||
if font_path:
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
font_large = ImageFont.truetype(font_path, font_size_large)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
font_large = font
|
||||
else:
|
||||
# 尝试使用项目目录中的字体
|
||||
try:
|
||||
# 检查当前目录和上级目录中的字体文件
|
||||
possible_fonts = [
|
||||
"SIMHEI.TTF",
|
||||
"../SIMHEI.TTF",
|
||||
"../../SIMHEI.TTF",
|
||||
os.path.join(os.path.dirname(__file__), "..", "SIMHEI.TTF")
|
||||
]
|
||||
font_loaded = False
|
||||
for font_file in possible_fonts:
|
||||
if os.path.exists(font_file):
|
||||
font = ImageFont.truetype(font_file, font_size)
|
||||
font_large = ImageFont.truetype(font_file, font_size_large)
|
||||
font_loaded = True
|
||||
break
|
||||
|
||||
if not font_loaded:
|
||||
# 尝试系统字体
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", font_size)
|
||||
font_large = ImageFont.truetype("arial.ttf", font_size_large)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
font_large = font
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
font_large = font
|
||||
|
||||
# 绘制二维码
|
||||
# 计算需要绘制的总位置数,确保至少包含一次完整字符串
|
||||
total_positions = 0
|
||||
for y, row in enumerate(matrix):
|
||||
for x, bit in enumerate(row):
|
||||
if bit:
|
||||
total_positions += 1
|
||||
|
||||
# 如果字符串长度大于总位置数,调整字符串以确保覆盖
|
||||
effective_charset = charset
|
||||
if len(charset) > total_positions and total_positions > 0:
|
||||
# 如果字符串太长,截取到能完整显示的长度
|
||||
effective_charset = charset[:total_positions]
|
||||
|
||||
# 开始绘制,使用优化后的字符分配
|
||||
position_counter = 0
|
||||
for y, row in enumerate(matrix):
|
||||
for x, bit in enumerate(row):
|
||||
|
||||
if bit:
|
||||
x_pos = x * cell_size
|
||||
y_pos = y * cell_size
|
||||
|
||||
# 使用位置计数器来确保字符串的完整性
|
||||
ch = effective_charset[position_counter % len(effective_charset)]
|
||||
char_index = position_counter % len(effective_charset)
|
||||
position_counter += 1
|
||||
|
||||
# 根据渲染模式绘制不同内容
|
||||
if render_mode == "solid":
|
||||
# 只绘制黑色方块
|
||||
draw.rectangle(
|
||||
[x_pos, y_pos, x_pos + cell_size - 1, y_pos + cell_size - 1],
|
||||
fill="black"
|
||||
)
|
||||
|
||||
elif render_mode == "text":
|
||||
# 检查是否为定位点区域
|
||||
if is_finder_pattern(x, y, N):
|
||||
# 定位点区域绘制黑色方块
|
||||
draw.rectangle(
|
||||
[x_pos, y_pos, x_pos + cell_size - 1, y_pos + cell_size - 1],
|
||||
fill="black"
|
||||
)
|
||||
else:
|
||||
# 非定位点区域绘制文字(黑色文字在白色背景上)
|
||||
# 判断字符类型
|
||||
is_chinese = '\u4e00' <= ch <= '\u9fff'
|
||||
is_uppercase = ch.isupper()
|
||||
is_lowercase = ch.islower()
|
||||
is_digit = ch.isdigit()
|
||||
|
||||
# 判断是否是第一个或最后一个字符,选择对应字体
|
||||
# 只有第一次循环的首尾字符才放大
|
||||
is_first_cycle = position_counter <= len(effective_charset)
|
||||
is_first_or_last_in_cycle = char_index == 0 or char_index == len(effective_charset) - 1
|
||||
is_first_or_last = is_first_cycle and is_first_or_last_in_cycle
|
||||
current_font = font_large if is_first_or_last else font
|
||||
|
||||
# 使用方块的中心点作为文字的中心
|
||||
center_x = x_pos + cell_size // 2
|
||||
center_y = y_pos + cell_size // 2
|
||||
|
||||
try:
|
||||
# 根据字符类型调整垂直偏移
|
||||
if is_chinese:
|
||||
# 中文字符直接居中
|
||||
draw.text(
|
||||
(center_x, center_y),
|
||||
ch,
|
||||
font=current_font,
|
||||
fill="black",
|
||||
anchor="mm"
|
||||
)
|
||||
else:
|
||||
# 英文和数字需要特殊处理
|
||||
# 获取文字边界框以进行精确调整
|
||||
bbox = draw.textbbox((0, 0), ch, font=current_font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# 计算基础位置
|
||||
text_x = center_x - text_width // 2 - bbox[0]
|
||||
text_y = center_y - text_height // 2 - bbox[1]
|
||||
|
||||
# 根据字符类型进行垂直调整
|
||||
if is_uppercase or is_digit:
|
||||
# 大写字母和数字向上偏移
|
||||
text_y -= int(cell_size * 0.05)
|
||||
elif is_lowercase:
|
||||
# 小写字母可能需要不同的调整
|
||||
text_y -= int(cell_size * 0.03)
|
||||
|
||||
draw.text((text_x, text_y), ch, font=current_font, fill="black")
|
||||
|
||||
except Exception as e:
|
||||
# 如果出现任何错误,使用简单的中心定位
|
||||
bbox = draw.textbbox((0, 0), ch, font=current_font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
text_x = center_x - text_width // 2
|
||||
text_y = center_y - text_height // 2
|
||||
draw.text((text_x, text_y), ch, font=current_font, fill="black")
|
||||
|
||||
elif render_mode == "combined":
|
||||
# 先绘制黑色方块
|
||||
draw.rectangle(
|
||||
[x_pos, y_pos, x_pos + cell_size - 1, y_pos + cell_size - 1],
|
||||
fill="black"
|
||||
)
|
||||
|
||||
# 在黑色方块上绘制灰色文字 RGB(102, 102, 102)
|
||||
# 判断字符类型
|
||||
is_chinese = '\u4e00' <= ch <= '\u9fff'
|
||||
is_uppercase = ch.isupper()
|
||||
is_lowercase = ch.islower()
|
||||
is_digit = ch.isdigit()
|
||||
|
||||
# 使用方块的中心点作为文字的中心
|
||||
center_x = x_pos + cell_size // 2
|
||||
center_y = y_pos + cell_size // 2
|
||||
|
||||
try:
|
||||
# 根据字符类型调整垂直偏移
|
||||
if is_chinese:
|
||||
# 中文字符直接居中
|
||||
draw.text(
|
||||
(center_x, center_y),
|
||||
ch,
|
||||
font=font,
|
||||
fill="black",
|
||||
anchor="mm"
|
||||
)
|
||||
else:
|
||||
# 英文和数字需要特殊处理
|
||||
# 获取文字边界框以进行精确调整
|
||||
bbox = draw.textbbox((0, 0), ch, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# 计算基础位置
|
||||
text_x = center_x - text_width // 2 - bbox[0]
|
||||
text_y = center_y - text_height // 2 - bbox[1]
|
||||
|
||||
# 根据字符类型进行垂直调整
|
||||
if is_uppercase or is_digit:
|
||||
# 大写字母和数字向上偏移
|
||||
text_y -= int(cell_size * 0.05)
|
||||
elif is_lowercase:
|
||||
# 小写字母可能需要不同的调整
|
||||
text_y -= int(cell_size * 0.03)
|
||||
|
||||
draw.text((text_x, text_y), ch, font=font, fill="black")
|
||||
|
||||
except Exception as e:
|
||||
# 如果出现任何错误,使用简单的中心定位
|
||||
bbox = draw.textbbox((0, 0), ch, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
text_x = center_x - text_width // 2
|
||||
text_y = center_y - text_height // 2
|
||||
draw.text((text_x, text_y), ch, font=font, fill="black")
|
||||
|
||||
img.save(out_path)
|
||||
return img
|
||||
|
||||
def render_to_svg(
|
||||
matrix,
|
||||
out_path: str,
|
||||
cell_size: int = 20,
|
||||
charset: str = DEFAULT_CHARSET,
|
||||
font_family: str = "Courier New",
|
||||
):
|
||||
# 去掉QR码的4像素边框
|
||||
border = 4
|
||||
original_size = len(matrix)
|
||||
|
||||
# 裁剪矩阵,去掉边框
|
||||
cropped_matrix = []
|
||||
for y in range(border, original_size - border):
|
||||
row = []
|
||||
for x in range(border, original_size - border):
|
||||
row.append(matrix[y][x])
|
||||
cropped_matrix.append(row)
|
||||
|
||||
matrix = cropped_matrix
|
||||
N = len(matrix)
|
||||
size = N * cell_size
|
||||
dwg = svgwrite.Drawing(out_path, size=(size, size), profile="tiny")
|
||||
|
||||
# 计算字符分配
|
||||
total_positions = sum(sum(row) for row in matrix)
|
||||
effective_charset = charset
|
||||
if len(charset) > total_positions and total_positions > 0:
|
||||
effective_charset = charset[:total_positions]
|
||||
|
||||
position_counter = 0
|
||||
for y, row in enumerate(matrix):
|
||||
for x, bit in enumerate(row):
|
||||
if bit:
|
||||
x_pos = x * cell_size
|
||||
y_pos = y * cell_size
|
||||
|
||||
# 检查是否为定位点区域
|
||||
if is_finder_pattern(x, y, N):
|
||||
# 定位点区域只绘制黑色方块
|
||||
dwg.add(
|
||||
dwg.rect(
|
||||
insert=(x_pos, y_pos),
|
||||
size=(cell_size, cell_size),
|
||||
fill="black"
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 非定位点区域绘制字符
|
||||
ch = effective_charset[position_counter % len(effective_charset)]
|
||||
position_counter += 1
|
||||
|
||||
# 调整 y 位置以实现垂直居中效果
|
||||
text_y = y_pos + cell_size * 0.65
|
||||
dwg.add(
|
||||
dwg.text(
|
||||
ch,
|
||||
insert=(x_pos + cell_size * 0.5, text_y),
|
||||
font_size=cell_size * 0.6,
|
||||
font_family=font_family,
|
||||
fill="black",
|
||||
text_anchor="middle"
|
||||
)
|
||||
)
|
||||
dwg.save()
|
||||
return out_path
|
||||
|
||||
# Convenience wrappers
|
||||
def generate_qr_image(data: str, **kwargs):
|
||||
matrix = generate_matrix(data, kwargs.pop("ec_level", "H"))
|
||||
return render_to_png(matrix, kwargs.get("out_path", "qr.png"), **kwargs)
|
||||
|
||||
def generate_qr_svg(data: str, **kwargs):
|
||||
matrix = generate_matrix(data, kwargs.pop("ec_level", "H"))
|
||||
return render_to_svg(matrix, kwargs.get("out_path", "qr.svg"), **kwargs)
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
qrcode>=7.4.2
|
||||
pillow>=10.0.0
|
||||
svgwrite>=1.4.3
|
Loading…
Reference in New Issue
Block a user