好友
阅读权限20
听众
最后登录1970-1-1
|
import os
import re
import zipfile
import io
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from xml.etree import ElementTree as ET
from PIL import Image as PIL_Image
import openpyxl
from openpyxl.drawing.image import Image
def get_dispimg_mapping(xlsx_path):
"""解析 WPS 特有的 cellimages.xml,得到 {ID: PIL_Image} 映射"""
mapping = {}
try:
with zipfile.ZipFile(xlsx_path) as zf:
if 'xl/cellimages.xml' not in zf.namelist() or 'xl/_rels/cellimages.xml.rels' not in zf.namelist():
return mapping
# ==================== 1. 解析 cellimages.xml ====================
ns = {
'xdr': 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing',
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
}
xml_data = zf.read('xl/cellimages.xml')
root = ET.fromstring(xml_data)
id_to_rid = {}
for pic in root.findall('.//xdr:pic', namespaces=ns):
cnv_pr = pic.find('.//xdr:cNvPr', namespaces=ns)
if cnv_pr is not None:
img_id = cnv_pr.get('name') # 如 ID_75BC16877A1847729B2E098D0D27FECE
if img_id and img_id.startswith('ID_'):
blip = pic.find('.//a:blip', namespaces=ns)
if blip is not None:
rid = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
if rid:
id_to_rid[img_id] = rid
# ==================== 2. 解析 cellimages.xml.rels ====================
rels_ns = {'': 'http://schemas.openxmlformats.org/package/2006/relationships'}
rels_data = zf.read('xl/_rels/cellimages.xml.rels')
rels_root = ET.fromstring(rels_data)
rid_to_target = {}
for rel in rels_root.findall('Relationship', namespaces=rels_ns):
rid = rel.get('Id')
target = rel.get('Target')
if rid and target and target.startswith('media/'):
rid_to_target[rid] = target
# ==================== 3. 构建最终映射 ====================
for img_id, rid in id_to_rid.items():
if rid in rid_to_target:
media_path = f"xl/{rid_to_target[rid]}"
if media_path in zf.namelist():
img_bytes = zf.read(media_path)
pil_img = PIL_Image.open(io.BytesIO(img_bytes))
mapping[img_id] = pil_img
print(f"✓ 映射成功: {img_id} → {rid_to_target[rid]}")
except Exception as e:
print(f"解析图片映射出错: {e}")
return mapping
def replace_dispimg(input_path):
"""核心处理函数:替换 dispimg 为真实图片"""
if not os.path.exists(input_path):
raise FileNotFoundError("文件不存在")
mapping = get_dispimg_mapping(input_path)
if not mapping:
print("⚠ 未找到 dispimg 图片,直接复制原文件")
dir_name = os.path.dirname(input_path)
base = os.path.basename(input_path)
name, ext = os.path.splitext(base)
output_path = os.path.join(dir_name, f"{name}_结果{ext}")
import shutil
shutil.copy2(input_path, output_path)
return output_path
# 加载工作簿(必须 data_only=False 才能读到公式)
wb = openpyxl.load_workbook(input_path, data_only=False)
replaced_count = 0
for ws in wb.worksheets:
print(f"处理工作表: {ws.title}")
for row in ws.iter_rows():
for cell in row:
if not isinstance(cell.value, str):
continue
formula = cell.value.strip()
# ==================== 关键正则(已适配真实 WPS 公式) ====================
# 支持: =DISPIMG("ID_xxx",1) =DISPIMG("ID_xxx";1) 带空格、带@ _xlfn. 等
match = re.search(
r'DISPIMG\s*\(\s*["\']?([^"\',;)]+)["\']?\s*[,;]?\s*\d*\s*\)',
formula,
re.IGNORECASE
)
if match:
img_id = match.group(1).strip()
print(f"🔍 发现 dispimg: {cell.coordinate} → {img_id}")
if img_id in mapping:
# 1. 清空原公式(防止 Excel 显示 #NAME?)
cell.value = None
# 2. 插入真实图片(自动成为 Excel 标准浮动图片)
pil_img = mapping[img_id]
img_obj = Image(pil_img)
# 可选:让图片自动适应单元格大小(推荐打开)
# img_obj.width = int(ws.column_dimensions[cell.column_letter].width * 7.5) if ws.column_dimensions[cell.column_letter].width else 100
# img_obj.height = int(ws.row_dimensions[cell.row].height * 1.3) if ws.row_dimensions[cell.row].height else 100
ws.add_image(img_obj, cell.coordinate)
replaced_count += 1
print(f"✅ 已替换: {cell.coordinate}")
else:
print(f"⚠ 未找到图片: {img_id}")
# 保存结果文件
dir_name = os.path.dirname(input_path)
base = os.path.basename(input_path)
name, ext = os.path.splitext(base)
output_path = os.path.join(dir_name, f"{name}_结果{ext}")
wb.save(output_path)
print(f"\n🎉 处理完成!共替换 {replaced_count} 张图片\n结果文件:{output_path}")
return output_path
# ====================== GUI(支持拖入 + 按钮选择) ======================
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("WPS dispimg 一键转 Excel 标准图片工具")
self.geometry("680x480")
self.resizable(False, False)
tk.Label(self, text="WPS 嵌入单元格图片修复工具", font=("微软雅黑", 16, "bold")).pack(pady=15)
self.drop_label = tk.Label(
self,
text="拖入 XLSX 文件到这里\n或点击下方「选择文件」",
font=("微软雅黑", 14),
bg="#f0f0f0",
fg="#333",
width=60,
height=12,
relief="solid",
bd=3
)
self.drop_label.pack(pady=10)
btn_frame = tk.Frame(self)
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="选择文件", font=("微软雅黑", 12), width=18, height=2,
command=self.select_file).pack(side=tk.LEFT, padx=15)
tk.Button(btn_frame, text="退出", font=("微软雅黑", 12), width=18, height=2,
command=self.quit).pack(side=tk.LEFT, padx=15)
self.status = ttk.Progressbar(self, mode='indeterminate')
self.status.pack(fill=tk.X, padx=30, pady=8)
self.info = tk.Label(self, text="", fg="green", font=("微软雅黑", 11), wraplength=600)
self.info.pack(pady=5)
# 尝试启用拖放(可选)
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
self.drop_label.drop_target_register(DND_FILES)
self.drop_label.dnd_bind('<<Drop>>', self.on_drop)
self.drop_label.config(text="支持直接拖入 XLSX 文件\n(已启用拖放功能)")
except ImportError:
self.drop_label.config(text="拖入功能需额外安装:\npip install tkinterdnd2\n\n或直接点击下方按钮选择")
def on_drop(self, event):
file_path = event.data.strip('{}')
self.process_file(file_path)
def select_file(self):
file_path = filedialog.askopenfilename(
title="选择含 dispimg 的 WPS 文件",
filetypes=[("Excel 文件", "*.xlsx *.xlsm")]
)
if file_path:
self.process_file(file_path)
def process_file(self, file_path):
if not file_path.lower().endswith(('.xlsx', '.xlsm')):
messagebox.showerror("错误", "请选择 .xlsx 或 .xlsm 文件!")
return
self.info.config(text="正在解析并替换 dispimg,请稍候...", fg="blue")
self.status.start()
try:
result_path = replace_dispimg(file_path)
self.status.stop()
self.info.config(text=f"✅ 处理完成!\n{os.path.basename(result_path)}", fg="green")
messagebox.showinfo("成功", f"已生成结果文件:\n{os.path.basename(result_path)}\n\n原文件未被修改")
except Exception as e:
self.status.stop()
self.info.config(text="❌ 处理失败", fg="red")
messagebox.showerror("错误", f"处理失败:{str(e)}")
if __name__ == "__main__":
print("=== WPS dispimg 修复工具 ===")
print("依赖安装命令:")
print("pip install openpyxl pillow")
print("(可选拖放)pip install tkinterdnd2\n")
app = App()
app.mainloop()
安装依赖:Bashpip install openpyxl pillow
把上面的代码保存为 wps_dispimg_fix.py,双击运行(或 python wps_dispimg_fix.py)。
把你的 .xlsx 文件拖入窗口灰色区域(或点击「选择文件」)。
程序自动在同目录生成 原文件名_结果.xlsx。 |
|