from concurrent.futures import ThreadPoolExecutor import math import os import re import sys import time from bs4 import BeautifulSoup import threading import tkinter as tk from tkinter import messagebox # 这个是旧码带生成。组合顺序不变:1,2, 1,3, 1,4, ... # 码带实际长度只由输入的起点、终点决定,码本大小按终点自动计算 def get_app_dir(): if getattr(sys, 'frozen', False): return os.path.dirname(os.path.abspath(sys.executable)) return os.path.dirname(os.path.abspath(__file__)) # 定义常量 i = 1 j = 0 scale_count = 0 m2cm = 100 APP_DIR = get_app_dir() HTML_FOLDER = os.path.join(APP_DIR, 'code_ims') OUTPUT_FOLDER = os.path.join(APP_DIR, 'QRTape') # 每个码的 SVG 只解析、缩放一次 _processed_svg_cache = {} # 修改二维码的SVG值 def data_scaling(svg_content): replacements = { "40": "7.092198581560284", "80": "14.18439716312057", "120": "21.27659574468085", "160": "28.36879432624113", "200": "35.46099290780142", "240": "42.55319148936170", "280": "49.64539007092199", "320": "56.73758865248227", "400": "56.73758865248227", "420": "56.73758865248227" } # 替换函数 def replace_text(text): for old, new in replacements.items(): text = re.sub(r'\b' + re.escape(old) + r'\b', new, text) return text return replace_text(svg_content) # 将svg与二维码的起点对齐 subtract_value = 7.092198581560284 # 定义正则表达式模式,用于匹配 x 和 y 属性 pattern = re.compile(r'(x|y)="\d+(\.\d+)?"') # 定义一个替换函数,用于减去指定值 def subtract_coords(match): attr = match.group(1) value = float(match.group(0).split('=')[1].strip('"')) new_value = value - subtract_value return f'{attr}="{new_value:.4f}"' # 获取二维码的 SVG 数据 def get_qr_code_svg(html_file): with open(html_file, 'r', encoding='utf-8') as file: html_content = file.read() soup = BeautifulSoup(html_content, 'html.parser') for text_tag in soup.find_all('text'): text_tag.decompose() svgs = soup.find_all('svg') all_svg_content = "" for svg in svgs: all_svg_content += str(svg) + "\n" return all_svg_content def get_processed_svg(code_id): """同一编号的码只读取并处理一次。""" cached = _processed_svg_cache.get(code_id) if cached is not None: return cached html_file = os.path.join(HTML_FOLDER, f'im{code_id}.html') svg_contents = get_qr_code_svg(html_file) scaled_svg_content = data_scaling(svg_contents) updated_svg_content = pattern.sub(subtract_coords, scaled_svg_content) _processed_svg_cache[code_id] = updated_svg_content return updated_svg_content def save_svg_to_html(svg_content, output_path, length): """将 SVG 内容保存到 HTML 文件中""" tick_mark_html = [] tick_interval = 189.1252955082742 # 每隔188像素一个刻度线 outer_container_width = length * 56.73758865248227 + (length - 1) * 18.91252955082742 + 18.91252955082742 max_ticks = int(outer_container_width // tick_interval) + 1 start_cm = int(require_length_start * m2cm) for i in range(0, max_ticks): tick_position = i * tick_interval if tick_position <= outer_container_width: tick_mark_html.append(f'
\n') tick_label = start_cm + 5 * i tick_mark_html.append(f'
{tick_label}
\n') html_content = f"""
{svg_content}
{''.join(tick_mark_html)}
""" with open(output_path, 'w', encoding='utf-8') as file: file.write(html_content) print(f"html 文件已保存至" + output_path) def codebook_size_for_end(end_m): """旧组合下,覆盖到终点所需的最小码数量:n*(n-1) 厘米 >= 终点厘米。""" end_idx = int(end_m * m2cm // 2) needed = 2 * end_idx if needed <= 2: return 2 n = math.ceil((1 + math.sqrt(1 + 4 * needed)) / 2) while n * (n - 1) < needed: n += 1 return n def build_code_rows(code_count): """按旧逻辑生成双排码 ID:先 (i,j) 依次展开,再对半拆成上下两排。""" result_all = [] for i in range(1, code_count + 1): for j in range(i + 1, code_count + 1): result_all.append(i) result_all.append(j) half = code_count * (code_count - 1) // 2 return result_all[:half], result_all[half:] def create_html_with_qrcodes(output_path, codebook_end=None): try: start_idx = int(require_length_start * m2cm // 2) end_idx = int(require_length_end * m2cm // 2) row_len = end_idx - start_idx if row_len <= 0: raise ValueError("所选区间没有可生成的码带内容") if codebook_end is None: codebook_end = require_length_end code_count = codebook_size_for_end(codebook_end) max_code_file = os.path.join(HTML_FOLDER, f'im{code_count}.html') if not os.path.exists(max_code_file): raise ValueError(f"终点过长,缺少码文件 im{code_count}.html") result_all_1, result_all_2 = build_code_rows(code_count) row1 = result_all_1[start_idx:start_idx + row_len] row2 = result_all_2[start_idx:start_idx + row_len] start_time = time.perf_counter() print("开始处理,请稍等...") needed_ids = set(row1) needed_ids.update(row2) def preload(code_id): get_processed_svg(code_id) with ThreadPoolExecutor(max_workers=min(8, os.cpu_count() or 4)) as executor: list(executor.map(preload, needed_ids)) svg_parts = [get_processed_svg(code_id) for code_id in row1] svg_parts.extend(get_processed_svg(code_id) for code_id in row2) save_svg_to_html(''.join(svg_parts), output_path, row_len) print("所有码带段处理完成") elapsed_time = (time.perf_counter() - start_time) * 1000 print(f"段落处理执行耗时: {elapsed_time:.4f} 毫秒") except Exception as e: print(f"生成码带时发生错误: {str(e)}") raise def _decimal_places(value): text = str(value) if '.' not in text: return 0 return len(text.split('.')[1]) def _format_m(value): return f"{value:.1f}" def _parse_range(start_text, end_text, start_label="码带起点", end_label="码带终点"): start = float(start_text) end = float(end_text) if _decimal_places(start) > 2 or _decimal_places(end) > 2: raise ValueError("最多保留小数点后两位,请重新输入!") if start < 0: raise ValueError(f"{start_label}必须大于或等于零") if end <= 0: raise ValueError(f"{end_label}必须大于零") if end <= start: raise ValueError(f"{end_label}必须大于{start_label}") return start, end def _output_dir(): output_html_path = os.path.normpath(OUTPUT_FOLDER) if not os.path.exists(output_html_path): os.makedirs(output_html_path) if not os.path.exists(output_html_path): raise FileNotFoundError(f"输出文件夹路径不存在: {output_html_path}") if not os.path.exists(HTML_FOLDER): raise FileNotFoundError(f"请将 code_ims 文件夹放在程序同一目录下:\n{HTML_FOLDER}") return output_html_path def run_gui(): mode = {"value": "parent"} parent_range = {"start": None, "end": None} last_output_dir = {"path": None} def show_open_folder_button(folder_path): last_output_dir["path"] = folder_path if not btn_open.winfo_ismapped(): btn_open.pack(pady=5) def on_open_folder(): folder_path = last_output_dir["path"] if not folder_path or not os.path.exists(folder_path): messagebox.showerror("错误", "还没有生成码带,或文件夹不存在") return os.startfile(folder_path) def start_generate(file_name, codebook_end): output_html_path = _output_dir() output_file = os.path.normpath(os.path.join(output_html_path, file_name)) def show_loading_window(): loading_window = tk.Toplevel(root) loading_window.title("生成码带") loading_window.geometry("300x100") label = tk.Label(loading_window, text="正在生成码带中,请稍等......", font=("Arial", 14)) label.pack(pady=30) loading_window.update_idletasks() try: create_html_with_qrcodes(output_file, codebook_end=codebook_end) except Exception as e: loading_window.destroy() messagebox.showerror("错误", f"生成码带时发生错误: {str(e)}") return loading_window.destroy() root.after(0, show_open_folder_button, output_html_path) messagebox.showinfo("成功", "码带生成成功,文件已保存至程序目录下QRTape文件夹") threading.Thread(target=show_loading_window, daemon=True).start() def on_generate(): try: global require_length_start, require_length_end if mode["value"] == "parent": require_length_start, require_length_end = _parse_range( entry_start.get(), entry_end.get() ) file_name = f"{_format_m(require_length_start)}_{_format_m(require_length_end)}.html" start_generate(file_name, codebook_end=require_length_end) return sub_start, sub_end = _parse_range( entry_start.get(), entry_end.get(), "子码带起点", "子码带终点" ) parent_start = parent_range["start"] parent_end = parent_range["end"] if sub_start < parent_start or sub_end > parent_end: raise ValueError( f"子码带必须在母码带 {_format_m(parent_start)}~{_format_m(parent_end)} m 范围内" ) require_length_start = sub_start require_length_end = sub_end file_name = ( f"{_format_m(parent_start)}_{_format_m(parent_end)}" f"_sub_{_format_m(sub_start)}_{_format_m(sub_end)}.html" ) start_generate(file_name, codebook_end=parent_end) except ValueError as e: messagebox.showerror("错误", f"输入的码带长度无效: {str(e)}") except Exception as e: messagebox.showerror("错误", f"发生错误: {str(e)}") def show_sub_page(): try: parent_start, parent_end = _parse_range(entry_start.get(), entry_end.get()) except ValueError as e: messagebox.showerror("错误", f"请先输入有效的母码带起点和终点: {str(e)}") return except Exception as e: messagebox.showerror("错误", f"发生错误: {str(e)}") return parent_range["start"] = parent_start parent_range["end"] = parent_end mode["value"] = "sub" label_start.config(text="请输入子码带起点(m):") label_end.config(text="请输入子码带终点(m):") parent_info.config(text=f"母码带: {_format_m(parent_start)} ~ {_format_m(parent_end)} m") entry_start.delete(0, tk.END) entry_end.delete(0, tk.END) btn_sub.pack_forget() btn_back.pack(pady=5) if last_output_dir["path"]: if btn_open.winfo_ismapped(): btn_open.pack_forget() btn_open.pack(pady=5) def show_parent_page(): mode["value"] = "parent" label_start.config(text="请输入码带起点(m):") label_end.config(text="请输入码带终点(m):") parent_info.config(text="") entry_start.delete(0, tk.END) entry_end.delete(0, tk.END) if parent_range["start"] is not None: entry_start.insert(0, str(parent_range["start"])) entry_end.insert(0, str(parent_range["end"])) btn_back.pack_forget() btn_sub.pack(pady=5) if last_output_dir["path"]: if btn_open.winfo_ismapped(): btn_open.pack_forget() btn_open.pack(pady=5) root = tk.Tk() root.title("旧码带生成器") root.geometry("320x380") label_start = tk.Label(root, text="请输入码带起点(m):", font=("Arial", 14)) label_start.pack(padx=10, pady=5) entry_start = tk.Entry(root, width=10, font=("Arial", 14)) entry_start.pack(padx=10, pady=5) label_end = tk.Label(root, text="请输入码带终点(m):", font=("Arial", 14)) label_end.pack(padx=10, pady=5) entry_end = tk.Entry(root, width=10, font=("Arial", 14)) entry_end.pack(padx=10, pady=5) parent_info = tk.Label(root, text="", font=("Arial", 11), fg="#444444") parent_info.pack(padx=10, pady=2) btn_generate = tk.Button(root, text="Click to Generate", command=on_generate, font=("Arial", 15)) btn_generate.pack(pady=10) btn_sub = tk.Button(root, text="生成子码带", command=show_sub_page, font=("Arial", 14)) btn_sub.pack(pady=5) btn_back = tk.Button(root, text="返回", command=show_parent_page, font=("Arial", 14)) btn_open = tk.Button(root, text="打开生成文件夹", command=on_open_folder, font=("Arial", 14)) root.mainloop() if __name__ == '__main__': run_gui()