Files
TapeGenerator/QRdoubletape_back.py
T
shuai.liandCursor ef1f59d41a Add TapeGenerator source code and EXE package with assets.
Include QRdoubletape scripts, packaged executables, and code_ims/QRTape resources for running the tape generator.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 13:13:54 +08:00

428 lines
15 KiB
Python

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'<div class="line" style="left: {tick_position}px;"></div>\n')
tick_label = start_cm + 5 * i
tick_mark_html.append(f'<div class="scale-value" style="left: {tick_position + 3}px;">{tick_label}</div>\n')
html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.outer-container1 {{
border-top: 2px solid black; /* 上边框 */
border-right: 2px solid black; /* 右边框 */
border-left: 6px solid black; /* 左边框加粗 */
border-bottom: 6px solid black; /* 下边框加粗 */
display: flex;
justify-content: center; /* 居中对齐内容 */
align-items: center; /* 居中对齐内容 */
margin: 0px; /* 可选:外边距以确保大框与页面其他部分之间有距离 */
position: absolute; /* 使用绝对定位 */
top: 10%; /* 从页面顶部 % 的位置开始 */
left: 2%; /* 从页面左侧 % 的位置开始 */
width: {outer_container_width}px; /* 显式设置大框的宽度 */
height: 169.4167px; /* 显式设置大框的高度 */
}}
.grid-container1 {{
display: grid;
position: absolute;
grid-template-columns: repeat({length}, 56.73758865248227px);
grid-template-rows: repeat(2, 56.73758865248227px);
grid-gap: 18.91252955082742px 18.91252955082742px;
padding: 0px;
left: 9.456264775413712px;
top: 18.91252955082742px;
}}
.line {{
position: absolute;
width: 2px; /* 刻度线的宽度 */
height: 10px; /* 刻度线的高度 */
background-color: black; /* 刻度线的颜色 */
bottom: 0; /* 将刻度线放置在容器的底部 */
}}
.scale-value {{
position: absolute;
bottom: -1px; /* 将数字放置在刻度线的上方 */
font-size: 12px; /* 字体大小 */
color: black; /* 字体颜色 */
transform: translateX(45%); /* 水平居中对齐数字 */
}}
</style>
</head>
<body>
<div class="outer-container1">
<div class="grid-container1" id="outerContainer">
{svg_content}
</div>
{''.join(tick_mark_html)}
</div>
</body>
</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()