import os
import subprocess
import tempfile
import shutil
import webbrowser
import threading
from pdf2image import convert_from_path
from PIL import Image, ImageFilter
import cv2
import numpy as np
import PySimpleGUI as sg
def pdf_to_svg(pdf_path, output_svg, dpi=300, threshold=None, progress_callback=None):
"""
将电力一次图PDF转换为SVG矢量图(带进度回调)
:param pdf_path: 输入PDF文件路径
:param output_svg: 输出SVG文件路径
:param dpi: 渲染分辨率
:param threshold: 二值化阈值
:param progress_callback: 进度回调函数
"""
try:
# 创建临时工作目录
with tempfile.TemporaryDirectory() as tmpdir:
if progress_callback:
progress_callback(10, "正在加载PDF文件...")
# Step 1: 将PDF转换为PNG
images = convert_from_path(
pdf_path,
dpi=dpi,
output_folder=tmpdir,
fmt='png',
thread_count=4
)
if not images:
raise ValueError("无法从PDF提取图像")
# 仅处理第一页
img_path = os.path.join(tmpdir, "page1.png")
images[0].save(img_path, 'PNG')
if progress_callback:
progress_callback(30, "处理图像中...")
# Step 2: 高级图像预处理
# 读取图像并转为灰度
img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
# 自动计算阈值(如果未提供)
if threshold is None:
threshold = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[0]
# 应用高斯模糊减少噪点
blurred = cv2.GaussianBlur(img, (3, 3), 0)
# 二值化处理(保持黑线白底)
_, binary_img = cv2.threshold(blurred, threshold, 255, cv2.THRESH_BINARY)
# 保存处理后的图像
binary_path = os.path.join(tmpdir, "binary.png")
cv2.imwrite(binary_path, binary_img)
if progress_callback:
progress_callback(60, "转换为矢量格式...")
# Step 3: 使用potrace转换为SVG(优化参数)
svg_temp_path = os.path.join(tmpdir, "temp.svg")
# 优化后的potrace参数
subprocess.run([
'potrace',
binary_path,
'-s', # 输出SVG格式
'-o', svg_temp_path,
'--flat', # 忽略透明效果
'--opaque', # 使背景不透明
'--turdsize', '10', # 忽略小于10像素的噪点
'--alphamax', '1', # 角点阈值(更平滑的曲线)
'--longcurve', # 优化长曲线
'--group' # 将路径分组
], check=True)
if progress_callback:
progress_callback(90, "保存最终SVG文件...")
# 复制到最终位置
shutil.copy(svg_temp_path, output_svg)
if progress_callback:
progress_callback(100, "转换完成!")
return True, "转换成功!"
except Exception as e:
return False, f"转换失败: {str(e)}"
def create_gui():
"""创建图形用户界面"""
# 设置主题
sg.theme("LightBlue3")
# 布局设计
layout = [
[sg.Text("PDF转SVG转换器", font=("Arial", 20), justification='center', expand_x=True)],
[sg.HorizontalSeparator()],
[
sg.Text("选择PDF文件:", size=(12, 1)),
sg.Input(key="-PDFFILE-", expand_x=True),
sg.FileBrowse(file_types=(("PDF文件", "*.pdf"),))
],
[
sg.Text("输出文件夹:", size=(12, 1)),
sg.Input(key="-OUTDIR-", expand_x=True),
sg.FolderBrowse()
],
[
sg.Text("输出文件名:", size=(12, 1)),
sg.Input(key="-OUTFILE-", default_text="output.svg", expand_x=True)
],
[sg.HorizontalSeparator()],
[
sg.Text("DPI:", size=(8, 1)),
sg.Slider(range=(100, 600), default_value=300, orientation='h', key="-DPI-", size=(20, 15)),
sg.Text("阈值:"),
sg.Input(key="-THRESHOLD-", size=(5, 1), default_text="自动"),
sg.Text("", key="-STATUS-", size=(20, 1))
],
[sg.ProgressBar(100, orientation='h', size=(40, 20), key='-PROGRESS-', expand_x=True)],
[sg.Text("", key="-MESSAGE-", size=(50, 2), text_color="blue")],
[
sg.Button("开始转换", key="-CONVERT-", size=(12, 1)),
sg.Button("打开文件夹", key="-OPENFOLDER-", disabled=True),
sg.Button("预览SVG", key="-PREVIEW-", disabled=True),
sg.Button("退出", key="-EXIT-", size=(8, 1))
]
]
# 创建窗口
window = sg.Window("电力一次图PDF转SVG工具", layout, resizable=True, finalize=True)
# 当前转换任务线程
conversion_thread = None
# 事件循环
while True:
event, values = window.read(timeout=100)
if event in (sg.WIN_CLOSED, "-EXIT-"):
break
elif event == "-CONVERT-":
# 验证输入
pdf_path = values["-PDFFILE-"]
out_dir = values["-OUTDIR-"]
out_file = values["-OUTFILE-"]
if not pdf_path:
sg.popup_error("请选择PDF文件!")
continue
if not out_dir:
sg.popup_error("请选择输出文件夹!")
continue
if not out_file:
sg.popup_error("请输入输出文件名!")
continue
# 处理阈值
try:
if values["-THRESHOLD-"].lower() == "自动":
threshold = None
else:
threshold = int(values["-THRESHOLD-"])
except:
sg.popup_error("阈值必须是整数或'自动'!")
continue
# 准备输出路径
output_svg = os.path.join(out_dir, out_file)
# 禁用转换按钮
window["-CONVERT-"].update(disabled=True)
window["-MESSAGE-"].update("转换中...", text_color="blue")
# 定义进度回调函数
def progress_callback(progress, message):
window["-PROGRESS-"].update(progress)
window["-MESSAGE-"].update(message)
window["-STATUS-"].update(f"{progress}%")
# 在单独的线程中执行转换
def conversion_task():
success, message = pdf_to_svg(
pdf_path,
output_svg,
dpi=int(values["-DPI-"]),
threshold=threshold,
progress_callback=progress_callback
)
# 更新UI
window.write_event_value("-CONVERSION_DONE-", (success, message))
conversion_thread = threading.Thread(target=conversion_task, daemon=True)
conversion_thread.start()
elif event == "-CONVERSION_DONE-":
success, message = values[event]
if success:
window["-MESSAGE-"].update(message, text_color="green")
window["-OPENFOLDER-"].update(disabled=False)
window["-PREVIEW-"].update(disabled=False)
else:
window["-MESSAGE-"].update(message, text_color="red")
window["-CONVERT-"].update(disabled=False)
elif event == "-OPENFOLDER-":
out_dir = values["-OUTDIR-"]
if out_dir and os.path.isdir(out_dir):
webbrowser.open(f"file://{out_dir}")
elif event == "-PREVIEW-":
out_dir = values["-OUTDIR-"]
out_file = values["-OUTFILE-"]
if out_dir and out_file:
output_svg = os.path.join(out_dir, out_file)
if os.path.exists(output_svg):
webbrowser.open(f"file://{output_svg}")
window.close()
if __name__ == "__main__":
# 检查依赖
try:
subprocess.run(['potrace', '--version'], capture_output=True, check=True)
except:
sg.popup_error("未找到potrace! 请确保已安装并添加到系统路径。\n\n"
"Windows: 下载potrace并添加到PATH\n"
"Mac: brew install potrace\n"
"Linux: sudo apt-get install potrace")
exit()
# 启动GUI
create_gui()