This commit is contained in:
齐斌 2025-07-21 15:35:35 +08:00
commit e6ec79ba64
9 changed files with 673 additions and 0 deletions

209
README.md Normal file
View File

@ -0,0 +1,209 @@
# QR-ASCII-QR
一个强大且灵活的二维码生成工具,能够创建独特的"字符二维码"——在传统二维码的基础上叠加自定义字符,既保持完整的扫描功能,又具有独特的视觉效果。
[![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://python.org)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
## ✨ 主要特色
### 🎯 多种渲染模式
- **纯黑块模式 (solid)**: 传统的黑白二维码
- **纯文字模式 (text)**: 仅使用字符构成的二维码(定位点保持黑块)
- **组合模式 (combined)**: 黑色背景上叠加字符,视觉效果最佳
### 🔧 强大的自定义功能
- **自定义字符集**: 支持数字、字母、汉字等任意字符组合
- **多种输出格式**: PNG 位图和 SVG 矢量图
- **字体自定义**: 支持 TrueType 字体文件
- **Logo 嵌入**: 可在二维码中心添加 LogoPNG 模式)
- **高纠错级别**: 默认 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

Binary file not shown.

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

103
main.py Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python
"""Commandline 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
View File

@ -0,0 +1,7 @@
from .generator import (
generate_matrix,
render_to_png,
render_to_svg,
generate_qr_image,
generate_qr_svg,
)

Binary file not shown.

Binary file not shown.

351
qr_ascii_qr/generator.py Normal file
View 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
View File

@ -0,0 +1,3 @@
qrcode>=7.4.2
pillow>=10.0.0
svgwrite>=1.4.3