|
|
@@ -4,6 +4,7 @@
|
|
|
PDF 条码合成工具(单文件输入,每页独立输出)
|
|
|
模板为图片,嵌入到输入文件每一页的指定位置
|
|
|
输出页面固定裁剪为 60mm × 50mm
|
|
|
+文件名取“第几段”可配置(从 1 开始计数)
|
|
|
"""
|
|
|
|
|
|
import tkinter as tk
|
|
|
@@ -27,8 +28,8 @@ class PDFMergeApp:
|
|
|
def __init__(self, root):
|
|
|
self.root = root
|
|
|
self.root.title("PDF 条码合成工具")
|
|
|
- self.root.geometry("780x600")
|
|
|
- self.root.minsize(700, 500)
|
|
|
+ self.root.geometry("820x680")
|
|
|
+ self.root.minsize(720, 560)
|
|
|
|
|
|
# 路径变量
|
|
|
self.input_var = tk.StringVar()
|
|
|
@@ -38,7 +39,12 @@ class PDFMergeApp:
|
|
|
# 缩放与偏移变量
|
|
|
self.scale_var = tk.StringVar(value="100")
|
|
|
self.offset_x_var = tk.StringVar(value="0")
|
|
|
- self.offset_y_var = tk.StringVar(value="100")
|
|
|
+ self.offset_y_var = tk.StringVar(value="120")
|
|
|
+
|
|
|
+ # 文件名取段配置(段号从 1 开始)
|
|
|
+ self.seg1_var = tk.StringVar(value="1") # 主段
|
|
|
+ self.seg2_var = tk.StringVar(value="4") # 拼接段
|
|
|
+ self.seg2_enable_var = tk.BooleanVar(value=True) # 是否启用拼接段
|
|
|
|
|
|
self.build_ui()
|
|
|
self.load_config()
|
|
|
@@ -70,6 +76,7 @@ class PDFMergeApp:
|
|
|
param_frame = ttk.LabelFrame(main_frame, text="合成参数(自动保存)", padding="10")
|
|
|
param_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
|
|
|
|
|
|
+ # —— 缩放 / 偏移 ——
|
|
|
ttk.Label(param_frame, text="缩放比例(% 输出页宽):").grid(row=0, column=0, sticky=tk.W, padx=4)
|
|
|
ttk.Entry(param_frame, textvariable=self.scale_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=4)
|
|
|
ttk.Label(param_frame, text="例:100 = 模板宽度填满 60mm").grid(row=0, column=2, sticky=tk.W, padx=4)
|
|
|
@@ -82,6 +89,20 @@ class PDFMergeApp:
|
|
|
ttk.Entry(param_frame, textvariable=self.offset_y_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=4)
|
|
|
ttk.Label(param_frame, text="相对于 50mm 页面左上角,0 表示贴顶部").grid(row=2, column=2, sticky=tk.W, padx=4)
|
|
|
|
|
|
+ # —— 文件名取段配置 ——
|
|
|
+ ttk.Separator(param_frame, orient="horizontal").grid(
|
|
|
+ row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=8
|
|
|
+ )
|
|
|
+
|
|
|
+ ttk.Label(param_frame, text="文件名 - 主段(第几段):").grid(row=4, column=0, sticky=tk.W, padx=4, pady=4)
|
|
|
+ ttk.Entry(param_frame, textvariable=self.seg1_var, width=10).grid(row=4, column=1, sticky=tk.W, padx=4, pady=4)
|
|
|
+ ttk.Label(param_frame, text="从 1 开始计数(1 = 第一行文字)").grid(row=4, column=2, sticky=tk.W, padx=4, pady=4)
|
|
|
+
|
|
|
+ ttk.Label(param_frame, text="文件名 - 拼接段(第几段):").grid(row=5, column=0, sticky=tk.W, padx=4, pady=4)
|
|
|
+ ttk.Entry(param_frame, textvariable=self.seg2_var, width=10).grid(row=5, column=1, sticky=tk.W, padx=4, pady=4)
|
|
|
+ ttk.Checkbutton(param_frame, text="启用拼接段(不存在时自动忽略)",
|
|
|
+ variable=self.seg2_enable_var).grid(row=5, column=2, sticky=tk.W, padx=4, pady=4)
|
|
|
+
|
|
|
# 开始按钮
|
|
|
btn_frame = ttk.Frame(main_frame)
|
|
|
btn_frame.grid(row=4, column=0, columnspan=3, pady=10)
|
|
|
@@ -91,14 +112,16 @@ class PDFMergeApp:
|
|
|
# 日志区域
|
|
|
ttk.Label(main_frame, text="处理日志:").grid(row=5, column=0, sticky=tk.W, pady=(8, 0))
|
|
|
self.log_text = scrolledtext.ScrolledText(
|
|
|
- main_frame, height=18, wrap=tk.WORD, state=tk.NORMAL, font=("Consolas", 10)
|
|
|
+ main_frame, height=16, wrap=tk.WORD, state=tk.NORMAL, font=("Consolas", 10)
|
|
|
)
|
|
|
self.log_text.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=4)
|
|
|
main_frame.rowconfigure(6, weight=1)
|
|
|
|
|
|
# 参数变更自动保存
|
|
|
- for var in (self.scale_var, self.offset_x_var, self.offset_y_var):
|
|
|
+ for var in (self.scale_var, self.offset_x_var, self.offset_y_var,
|
|
|
+ self.seg1_var, self.seg2_var):
|
|
|
var.trace_add("write", lambda *args: self.save_config())
|
|
|
+ self.seg2_enable_var.trace_add("write", lambda *args: self.save_config())
|
|
|
|
|
|
# ---------- 路径选择 ----------
|
|
|
def select_input(self):
|
|
|
@@ -135,6 +158,9 @@ class PDFMergeApp:
|
|
|
"scale_percent": self.scale_var.get(),
|
|
|
"offset_x": self.offset_x_var.get(),
|
|
|
"offset_y": self.offset_y_var.get(),
|
|
|
+ "seg1": self.seg1_var.get(),
|
|
|
+ "seg2": self.seg2_var.get(),
|
|
|
+ "seg2_enable": bool(self.seg2_enable_var.get()),
|
|
|
}
|
|
|
try:
|
|
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
|
@@ -154,6 +180,9 @@ class PDFMergeApp:
|
|
|
self.scale_var.set(config.get("scale_percent", "100"))
|
|
|
self.offset_x_var.set(config.get("offset_x", "0"))
|
|
|
self.offset_y_var.set(config.get("offset_y", "0"))
|
|
|
+ self.seg1_var.set(config.get("seg1", "1"))
|
|
|
+ self.seg2_var.set(config.get("seg2", "4"))
|
|
|
+ self.seg2_enable_var.set(bool(config.get("seg2_enable", True)))
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
@@ -180,30 +209,61 @@ class PDFMergeApp:
|
|
|
except Exception:
|
|
|
return ""
|
|
|
|
|
|
+ # ---------- 数字字符修正 ----------
|
|
|
+ @staticmethod
|
|
|
+ def fix_digits(s):
|
|
|
+ """
|
|
|
+ 对条码字体常见的 OCR 混淆字符做修正。
|
|
|
+ - 单字符映射:¨→0 ©→1 ª→2 «→3 ®→6
|
|
|
+ - ° 后紧跟一位数字:视为该数字(去掉 °)
|
|
|
+ - 单独 °:视为 0
|
|
|
+ """
|
|
|
+ if not s:
|
|
|
+ return s
|
|
|
+ table = {"¨": "0", "©": "1", "ª": "2", "«": "3", "®": "6"}
|
|
|
+ for k, v in table.items():
|
|
|
+ s = s.replace(k, v)
|
|
|
+ s = re.sub(r"°(\d)", r"\1", s) # °7 → 7
|
|
|
+ s = s.replace("°", "0") # 单独 ° → 0
|
|
|
+ return s
|
|
|
+
|
|
|
+ # ---------- 文件名生成(可配置段号) ----------
|
|
|
def get_filename_from_text(self, text):
|
|
|
"""
|
|
|
- 取第一段 + 第四段文字作为文件名
|
|
|
- 如果第四段不存在,只用第一段
|
|
|
- 如果第一段也不存在,返回空字符串
|
|
|
+ 按配置的段号(从 1 开始)取文字作为文件名。
|
|
|
+ - seg1(主段):必取;越界则返回空字符串
|
|
|
+ - seg2(拼接段):可选,启用且存在时用下划线拼到主段后面
|
|
|
+ 段之间用下划线连接。
|
|
|
"""
|
|
|
if not text:
|
|
|
return ""
|
|
|
+
|
|
|
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
|
if not lines:
|
|
|
return ""
|
|
|
- first = lines[1]
|
|
|
- fourth = lines[4] if len(lines) >= 5 else ""
|
|
|
- fourth = fourth.replace("¨", "0")
|
|
|
- fourth = fourth.replace("©", "1")
|
|
|
- fourth = fourth.replace("ª", "2")
|
|
|
- fourth = fourth.replace("«", "3")
|
|
|
- fourth = fourth.replace("°2", "4")
|
|
|
- fourth = fourth.replace("°2", "5")
|
|
|
- fourth = fourth.replace("®", "6")
|
|
|
- fourth = fourth.replace("°2", "7")
|
|
|
- fourth = fourth.replace("°", "8")
|
|
|
- fourth = fourth.replace("°2", "9")
|
|
|
- name = first
|
|
|
+
|
|
|
+ def pick(seg_no):
|
|
|
+ """段号从 1 开始;非法或越界返回空字符串"""
|
|
|
+ try:
|
|
|
+ n = int(str(seg_no).strip())
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return ""
|
|
|
+ if n < 1 or n > len(lines):
|
|
|
+ return ""
|
|
|
+ return lines[n - 1]
|
|
|
+
|
|
|
+ first = pick(self.seg1_var.get())
|
|
|
+ if not first:
|
|
|
+ return ""
|
|
|
+
|
|
|
+ parts = [self.fix_digits(first)]
|
|
|
+
|
|
|
+ if self.seg2_enable_var.get():
|
|
|
+ second = pick(self.seg2_var.get())
|
|
|
+ if second:
|
|
|
+ parts.append(self.fix_digits(second))
|
|
|
+
|
|
|
+ name = "_".join(parts)
|
|
|
return self.sanitize_filename(name)
|
|
|
|
|
|
def sanitize_filename(self, text):
|
|
|
@@ -236,6 +296,30 @@ class PDFMergeApp:
|
|
|
self.start_btn.config(state=tk.NORMAL)
|
|
|
return
|
|
|
|
|
|
+ # 校验段号
|
|
|
+ try:
|
|
|
+ seg1_no = int(self.seg1_var.get().strip() or "1")
|
|
|
+ except ValueError:
|
|
|
+ self.log("❌ 错误:文件名 - 主段必须是整数")
|
|
|
+ self.start_btn.config(state=tk.NORMAL)
|
|
|
+ return
|
|
|
+ if seg1_no < 1:
|
|
|
+ self.log("❌ 错误:文件名 - 主段必须 ≥ 1")
|
|
|
+ self.start_btn.config(state=tk.NORMAL)
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.seg2_enable_var.get():
|
|
|
+ try:
|
|
|
+ seg2_no = int(self.seg2_var.get().strip() or "4")
|
|
|
+ except ValueError:
|
|
|
+ self.log("❌ 错误:文件名 - 拼接段必须是整数")
|
|
|
+ self.start_btn.config(state=tk.NORMAL)
|
|
|
+ return
|
|
|
+ if seg2_no < 1:
|
|
|
+ self.log("❌ 错误:文件名 - 拼接段必须 ≥ 1")
|
|
|
+ self.start_btn.config(state=tk.NORMAL)
|
|
|
+ return
|
|
|
+
|
|
|
if not all([input_file, template_file, output_dir]):
|
|
|
self.log("❌ 错误:请完整填写输入文件、模板图片和输出文件夹")
|
|
|
self.start_btn.config(state=tk.NORMAL)
|
|
|
@@ -254,7 +338,11 @@ class PDFMergeApp:
|
|
|
self.log(f"📄 输入文件:{os.path.basename(input_file)}")
|
|
|
self.log(f"🖼 模板图片:{os.path.basename(template_file)}")
|
|
|
self.log(f"📐 输出页面尺寸:60mm × 50mm({TARGET_W:.1f}pt × {TARGET_H:.1f}pt)")
|
|
|
- self.log(f"⚙ 参数:缩放 {scale_percent}% 宽,偏移 ({offset_x}, {offset_y})\n")
|
|
|
+ self.log(f"⚙ 参数:缩放 {scale_percent}% 宽,偏移 ({offset_x}, {offset_y})")
|
|
|
+ if self.seg2_enable_var.get():
|
|
|
+ self.log(f"🏷 文件名取段:主段 = 第 {seg1_no} 段,拼接段 = 第 {seg2_no} 段\n")
|
|
|
+ else:
|
|
|
+ self.log(f"🏷 文件名取段:仅主段 = 第 {seg1_no} 段\n")
|
|
|
|
|
|
try:
|
|
|
# 获取模板图片尺寸
|
|
|
@@ -294,11 +382,13 @@ class PDFMergeApp:
|
|
|
doc, idx, overlay=False
|
|
|
)
|
|
|
|
|
|
- # 2) 计算模板目标矩形(缩放基准改为输出页面宽度)
|
|
|
+ # 2) 计算模板目标矩形(缩放基准为输出页面宽度)
|
|
|
target_w = TARGET_W * (scale_percent / 100.0)
|
|
|
scale = target_w / template_w
|
|
|
target_h = template_h * scale
|
|
|
- target_rect = fitz.Rect(offset_x, offset_y, offset_x + target_w, offset_y + target_h)
|
|
|
+ target_rect = fitz.Rect(offset_x, offset_y,
|
|
|
+ offset_x + target_w,
|
|
|
+ offset_y + target_h)
|
|
|
|
|
|
# 3) 叠加模板图片(前景)
|
|
|
new_page.insert_image(target_rect, filename=template_file)
|