"""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)