Files
spdm-backend/project/newExportWord.py

935 lines
36 KiB
Python
Raw Normal View History

2026-01-22 19:29:23 +08:00
import os
import json
import shutil
import re
import traceback
from lxml import etree
from datetime import datetime
import sys
import base64
from PIL import Image
import io
from docx.shared import Pt
from docx.oxml.ns import qn
from docx.shared import Emu, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx import Document
from docx.shared import RGBColor
from Tool import getPfm, modCsv
import modifyReport
def getJsonData(jsonFilePath):
"""获取JSON数据"""
if not os.path.exists(jsonFilePath):
print(f"Error:JSON文件路径 '{jsonFilePath}' 不存在,请确认")
return None
with open(jsonFilePath, 'r', encoding='utf-8') as jsonFile:
data = json.load(jsonFile)
return data
def getCsvData(csvFilePath):
"""获取CSV数据"""
data_dict = {}
if not os.path.exists(csvFilePath):
print(f"Error:CSV文件路径 '{csvFilePath}' 不存在,请确认")
return data_dict
with open(csvFilePath, 'r', encoding='utf-8') as csvFile:
lines = csvFile.readlines()
for line in lines:
if "," in line:
values = line.strip().split(',')
key = values[0]
data_dict[key] = values[1]
return data_dict
def getTaskType(taskName, projectInfo_dict):
"""根据工况名称获取分析项类型"""
taskType = projectInfo_dict.get("taskType", None)
if taskType is not None:
return taskType
else:
print(
f"Warning:projectInfo.json中不存在'taskType'字段,将根据工况名称'{taskName}'进行判断")
if "动力学" in taskName or taskName.startswith("dynamics"):
return "dynamics"
elif "静力学" in taskName or taskName.startswith("statics"):
return "statics"
elif "流体" in taskName or taskName.startswith("fluid"):
return "fluid"
elif "热分析" in taskName or taskName.startswith("thermal"):
return "thermal"
else:
print(f"Warning:未知的分析项类型 '{taskName}',默认返回 'statics'")
return "statics"
def wirteValueToPerformance(keyName, value, highValue, csvPath):
"""把值写入到output_values.csv中 用于生成performance.json"""
lines = []
if os.path.exists(csvPath):
with open(csvPath, 'r+', encoding='utf-8') as fr:
lines = fr.readlines()
found = False
for i in range(len(lines)):
if lines[i].startswith(keyName + ","):
lines[i] = f"{keyName},{value},{highValue}\n"
found = True
break
if not found:
lines.append(f"{keyName},{value},{highValue}\n")
with open(csvPath, 'w+', encoding='utf-8') as fw:
fw.writelines(lines)
def replace_text_in_paragraph(paragraph, data_dict, csvPath=None):
"""替换段落中的文本"""
text = paragraph.text
# 居中显示
# paragraph.alignment = 1
ngFlag = False
if "$" in text and "<" not in text and ">" not in text:
keys_list = re.findall(r'\$([a-zA-Z0-9_]+)', text)
for keyName in keys_list:
value = data_dict.get(keyName, None)
if value is not None:
placeholder = f"${keyName}"
text = text.replace(placeholder, str(value))
print(f"文本: {placeholder} -> {value}")
paragraph.text = text
elif "$" in text and "<" in text:
# 处理带有<>的占位符
start_index = text.find("$")
end_index = text.find("<", start_index)
if end_index != -1:
placeholder = text[start_index:end_index + 1]
keyName = placeholder[1:-1] # 去掉$和<>
value = data_dict.get(keyName, None)
highValue = text.split("<")[1]
if value is not None:
# 把标准值写入到output_values.csv中 用于生成performance.json
wirteValueToPerformance(keyName, value, highValue, csvPath)
if float(value) > float(highValue):
result = "NG"
# 设置红色
ngFlag = True
else:
result = "OK"
paragraph.text = result
if ngFlag:
for run in paragraph.runs:
run.font.color.rgb = RGBColor(255, 0, 0)
print(f"文本: {placeholder} -> {result}")
elif "$" in text and ">" in text:
# 处理带有<>的占位符
start_index = text.find("$")
end_index = text.find(">", start_index)
if end_index != -1:
placeholder = text[start_index:end_index + 1]
keyName = placeholder[1:-1] # 去掉$和<>
value = data_dict.get(keyName, None)
highValue = text.split(">")[1]
if value is not None:
if float(value) < float(highValue):
result = "NG"
# 设置红色
ngFlag = True
else:
result = "OK"
paragraph.text = result
if ngFlag:
for run in paragraph.runs:
run.font.color.rgb = RGBColor(255, 0, 0)
print(f"文本: {placeholder} -> {result}")
def replace_text_in_table(table, data_dict, csvPath):
"""替换表格中的文本"""
for row in table.rows:
for cell in row.cells:
# 递归处理单元格中的嵌套表格
if cell.tables:
for nested_table in cell.tables:
replace_text_in_table(nested_table, data_dict, csvPath)
# 处理当前单元格的段落文本
for paragraph in cell.paragraphs:
replace_text_in_paragraph(paragraph, data_dict, csvPath)
def replace_text_in_textbox(doc, data_dict):
"""替换文本框中的文本"""
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing',
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
'wps': 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape'
}
doc_modified = False # 整个文档是否被修改
for part_id, related_part in doc.part.related_parts.items():
if hasattr(related_part, 'blob'):
xml_content = related_part.blob
# 检查是否是 XML 内容(以 <?xml 或 < 开头)
if not xml_content.strip().startswith(b'<?xml') and not xml_content.strip().startswith(b'<'):
continue
try:
try:
content_str = xml_content.decode('utf-8')
except UnicodeDecodeError:
continue
# 检查是否包含 XML 声明或根元素
if not content_str.strip().startswith('<?xml') and not content_str.strip().startswith('<'):
continue
root = etree.fromstring(xml_content)
# 查找文本框
textboxes = root.xpath('//wps:txbx', namespaces=namespaces)
part_modified = False # 当前部件是否被修改
for textbox in textboxes:
# 提取文本内容
text_elements = textbox.xpath(
'.//w:t', namespaces=namespaces)
for elem in text_elements:
# print(f"原始文本: {elem.text}")
if elem.text and "$" in elem.text:
keyName = elem.text.split("$")[1]
value = data_dict.get(keyName, None)
if value is not None:
elem.text = elem.text.replace(
f"${keyName}", str(value))
print(f"文本框替换: ${keyName} -> {value}")
part_modified = True
doc_modified = True
# 关键步骤:将修改后的 XML 写回到部件中
if part_modified:
# 同时更新 _element 和 _blob
if hasattr(related_part, '_element'):
related_part._element = root
updated_xml = etree.tostring(
root, encoding='UTF-8', xml_declaration=True)
related_part._blob = updated_xml
except etree.XMLSyntaxError as e:
print(f"Warning:部件 {part_id} 不是有效的 XML: {e}")
continue
except Exception as e:
print(f"Warning:处理部件 {part_id} 时出错: {e}")
continue
def replaceConclusion(doc, data_dict):
"""替换结论中的文本"""
targeTable_list = doc.tables[0]._cells
try:
for targeTable in targeTable_list:
for paragraph in targeTable.paragraphs:
replace_text_in_paragraph(paragraph, data_dict)
except Exception as e:
pass
def getImageNameList(projectPath):
"""获取项目根目录下所有图片名"""
imageName_list = []
for file in os.listdir(projectPath):
if file.endswith(".png") or file.endswith(".jpg") or file.endswith(".jpeg"):
imageName_list.append(file)
print(f"{projectPath} 图片列表: {imageName_list}")
return imageName_list
def modifyImageName(imageName_list, targetImage_list):
"""检查是否所有图片都存在 不存在则修改图片名"""
existImage_list = []
for imageName in imageName_list:
if imageName in targetImage_list:
existImage_list.append(imageName)
else:
for targetImage in targetImage_list:
if targetImage in existImage_list:
continue
newImageName = targetImage
break
# 修改图片名
newImage = f"{projectPath}/{newImageName}"
oldImage = f"{projectPath}/{imageName}"
print(f"Info:修改图片名: {imageName} -> {newImageName}")
existImage_list.append(newImageName)
if os.path.exists(oldImage):
os.rename(oldImage, newImage)
def getPefData(performance_dict, csvPath, templatePath, templateType):
"""获取性能指标数据 写入csv文件"""
content_list = []
for key, value in performance_dict.items():
if key == "type":
continue
for k, item in value.items():
if k == "value":
line = f"{key},{item}\n"
content_list.append(line)
break
# 手动上传 根据ppt模板,修改key
# mapPptKeyPath = f"{templatePath}/mapPptKeyToPerfor.json"
# pptKeyData_dict = getJsonData(mapPptKeyPath)[templateType]
# targetKey_list = list(pptKeyData_dict.keys())
# for index, line in enumerate(content_list):
# keyName = line.split(",")[0]
# if keyName not in targetKey_list and index < len(targetKey_list):
# newKeyName = targetKey_list[index]
# value = line.split(",")[1]
# newLine = f"{newKeyName},{value}"
# content_list[index] = newLine
with open(csvPath, "w", encoding="utf-8") as f:
f.writelines(content_list)
print(f"写入csv文件: {csvPath}")
def inspect_document_structure(doc_path):
"""
检查文档结构找出所有形状的名称
"""
doc = Document(doc_path)
print("文档中的形状信息:")
for i, shape in enumerate(doc.inline_shapes):
element = shape._inline
doc_pr = element.find('.//wp:docPr', namespaces=element.nsmap)
if doc_pr is not None:
shape_name = doc_pr.get('name', '')
shape_id = doc_pr.get('id', '')
desc = doc_pr.get('descr', '')
print(f"形状 {i+1}:")
print(f" 名称: {shape_name}")
print(f" ID: {shape_id}")
print(f" 描述: {desc}")
print("-" * 30)
def generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict):
"""生成Word报告"""
# inspect_document_structure(reportFilePath)
doc = Document(reportFilePath)
# 处理页眉
for section in doc.sections:
if section.header:
for paragraph in section.header.paragraphs:
replace_text_in_paragraph(paragraph, data_dict, csvPath)
# 处理页眉中的表格
for table in section.header.tables:
replace_text_in_table(table, data_dict, csvPath)
# 替换图片
for rel in doc.part.rels.values():
if "image" in rel.reltype:
# print(rel.target_ref, rel.reltype)
# media/image3.png
imageItem = rel.target_ref.split("/")[-1]
picName = image_dict.get(imageItem, None)
if picName is not None and picName != ".png":
replace_image_path = os.path.join(dataPath, picName)
if os.path.exists(replace_image_path):
print(f"图片: {imageItem} -> {picName}")
rel.target_part._blob = open(
replace_image_path, "rb").read()
# 处理表格
for table in doc.tables:
replace_text_in_table(table, data_dict, csvPath)
# 处理结论
replaceConclusion(doc, data_dict)
# 处理文本框
replace_text_in_textbox(doc, data_dict)
# 保存文档
doc.save(reportFilePath)
print(f"Info:Word文档已创建: {reportFilePath}")
def generatePerformance(projectPath, csvPath, key_dict, templateType):
"""生成性能指标"""
performancePath = f"{projectPath}/performance.json"
if templateType in ["statics", "dynamics"]:
performance_dict = getPfm.generalPerformance(csvPath, key_dict)
elif templateType == "modal":
performance_dict = getPfm.generalPerformanceModal(csvPath, key_dict)
else:
performance_dict = {}
with open(performancePath, 'w', encoding='utf-8') as perfFile:
json.dump(performance_dict, perfFile, indent=4, ensure_ascii=False)
print(f"Info:性能指标已生成: {performancePath}")
def exportWord(projectPath, templatePath, loacase=None):
"""主函数"""
print(f"Info:开始生成工况: {loacase} 的Word报告...")
# 根据工况名称获取docx模板类型和后处理图片和图片框对应关系
templateType = getTaskType(loacase)
templateFilePath = f"{templatePath}/{templateType}_template.docx"
mapPicNamePath = f"{templatePath}/mapPicNameToPpt.json"
image_dict = getJsonData(mapPicNamePath).get(templateType, None)
print(f"Info:模板类型: {templateType}, 图片对应关系: {image_dict}")
if image_dict is None:
print(f"Warning:模板类型: {templateType} 不存在图片替换,请确认")
if not os.path.exists(templateFilePath):
print(f"Error:模板文件路径 '{templateFilePath}' 不存在,请确认")
return
# 拷贝模板文件
reportPath = f"{projectPath}/report"
if not os.path.exists(reportPath):
os.makedirs(reportPath)
reportFilePath = f"{reportPath}/report.docx"
shutil.copy(templateFilePath, reportFilePath)
# 获取项目信息数据和结果数据
projectInfoPath = f"{projectPath}/projectInfo.json"
projectInfo_dict = getJsonData(projectInfoPath)
dataPath = f"{projectPath}/result/process_data"
csvPath = f"{dataPath}/output_values.csv"
# 个别工况需要先对output_values.csv进行处理
if templateType == "thermal":
modCsv.modifyCsvForThermal(csvPath)
data_dict = getCsvData(csvPath)
data_dict.update(projectInfo_dict)
# 生成报告
generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict)
# 生成性能指标
mapPptKeyPath = f"{templatePath}/mapPptKeyToPerfor.json"
key_dict = getJsonData(mapPptKeyPath).get(templateType, None)
print(f"\nInfo:PPT键值中英文: {key_dict}")
generatePerformance(projectPath, csvPath, key_dict, templateType)
def exportWordByUser(projectPath, templatePath, loacase=None):
"""用户手动导出Word报告 图片和性能指标都是手动上传到项目路径根目录下"""
print(f"Info:开始生成工况: {loacase} 的Word报告...")
projectInfoPath = f"{projectPath}/projectInfo.json"
projectInfo_dict = getJsonData(projectInfoPath)
templateType = getTaskType(loacase, projectInfo_dict)
templateFilePath = f"{templatePath}/{templateType}_template.docx"
mapPicNamePath = f"{templatePath}/mapPicNameToPpt.json"
image_dict = getJsonData(mapPicNamePath).get(templateType, None)
targetImage_list = list(image_dict.values())
print(f"Info:模板类型: {templateType}, 图片对应关系: {image_dict}")
if image_dict is None:
print(f"Warning:模板类型: {templateType} 不存在图片替换,请确认")
if not os.path.exists(templateFilePath):
print(f"Error:模板文件路径 '{templateFilePath}' 不存在,请确认")
return
# 获取上传的图片名
imageName_list = getImageNameList(projectPath)
# 检查是否所有图片都存在 不存在则修改图片名
modifyImageName(imageName_list, targetImage_list)
# 拷贝模板文件
reportPath = f"{projectPath}/report"
if not os.path.exists(reportPath):
os.makedirs(reportPath)
reportFilePath = f"{reportPath}/report.docx"
shutil.copy(templateFilePath, reportFilePath)
# 获取项目信息数据
dataPath = projectPath
# 获取指标数据
performancePath = f"{projectPath}/performance.json"
performance_dict = getJsonData(performancePath)
csvPath = f"{dataPath}/output_values.csv"
getPefData(performance_dict, csvPath, templatePath, templateType)
data_dict = getCsvData(csvPath)
data_dict.update(projectInfo_dict)
# 生成报告
generateReport(reportFilePath, dataPath, data_dict, csvPath, image_dict)
class Tee:
"""自定义文件对象,同时写入文件和原控制台"""
def __init__(self, *files):
self.files = files
def write(self, obj):
for f in self.files:
f.write(obj)
f.flush() # 确保立即写入
def flush(self):
for f in self.files:
f.flush()
def base64ToImg(projectPath, reportContent_list):
"""把base64字符串转成图片"""
img_num = 0
imgName_dict = {}
for item_dict in reportContent_list:
if item_dict["type"] == "img":
key_name = item_dict.get("key", "")
if key_name not in imgName_dict:
imgName_dict[key_name] = []
img_base64_list = item_dict.get("value", [])
if img_base64_list:
for img_base64_dict in img_base64_list:
imgTemp_dict = {}
img_base64_str = img_base64_dict.get("src", "")
img_base64 = img_base64_str.split(",")[1]
img_data = base64.b64decode(img_base64)
img_num += 1
img_path = f"{projectPath}/pic{img_num}.png"
picName = img_base64_dict.get("title", "")
imgTemp_dict[f"pic{img_num}"] = picName
imgName_dict[key_name].append(imgTemp_dict)
with open(img_path, 'wb') as img_file:
img_file.write(img_data)
# 新增字典中的picPath为图片路径
# item_dict["picPath"] = img_path
return img_num, imgName_dict
def getDataDict(reportContent_list):
"""获取数据字典 用于替换文本"""
data_dict = {}
table_num = 0
for item_dict in reportContent_list:
if item_dict["type"] == "text":
data_dict[item_dict["key"]] = item_dict.get("value", "")
elif item_dict["type"] == "table":
tableContent = item_dict.get("value", [])
# 获取表头
headers = item_dict.get("head", [])
tableName = item_dict.get("key", "")
if headers and tableName:
header_str = ",".join(headers)
# newKey = item_dict["key"] + "_" + header_str
newKey = tableName + "_" + header_str
data_dict[newKey] = tableContent
table_num += 1
elif item_dict["type"] == "conclusion":
data_dict[item_dict["key"]] = item_dict["value"]
return data_dict, table_num
def set_table_border(table):
"""
通过底层 XML 强制给表格添加全框线细黑实线
"""
tbl = table._tbl
tblPr = tbl.tblPr
# 创建表格边框元素
borders = OxmlElement('w:tblBorders')
# 定义需要添加边框的方向:上下左右、内部水平、内部垂直
for border_name in ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']:
edge = OxmlElement(f'w:{border_name}')
edge.set(qn('w:val'), 'single') # 线型:单实线
edge.set(qn('w:sz'), '4') # 线宽1/8 磅的倍数4 表示 0.5 磅
edge.set(qn('w:space'), '0') # 间距
edge.set(qn('w:color'), 'auto') # 颜色:自动(黑色)
borders.append(edge)
tblPr.append(borders)
def replaceParagraph(doc, paragraph, data_dict):
"""替换段落中的占位符:删除原段落并新增表格"""
text = paragraph.text
if "$table_" in text:
judgeFalg = False
targets = {"分析值", "达成状态", "目标值"}
parts = text.split("_")
if len(parts) < 2:
return
tableName = parts[1]
for item_key, item_value in data_dict.items():
if tableName in item_key:
head_str = item_key.split("_")[1]
head_list = head_str.split(",")
if all(item in head_list for item in targets):
judgeFalg = True
value_list = [list(item_dict.values())
for item_dict in item_value]
table_content = [head_list] + value_list
# 创建新表格
rows = len(table_content)
cols = len(head_list)
table = doc.add_table(rows=rows, cols=cols)
set_table_border(table)
for row_index, row_data in enumerate(table_content):
valueIndex = None
targetIndex = None
judgeIndex = None
if row_index == 0 and judgeFalg:
# 记录目标值、达成状态、分析值的列索引
for col_index, cell_value in enumerate(row_data):
if cell_value == "目标值":
targetIndex = col_index
elif cell_value == "达成状态":
judgeIndex = col_index
elif cell_value == "分析值":
valueIndex = col_index
for col_index, cell_value in enumerate(row_data):
cell = table.cell(row_index, col_index)
cell.text = ""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(str(cell_value))
run.font.name = '微软雅黑'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')
run.font.size = Pt(10.5)
if row_index == 0:
run.bold = True
# 如果是不合格变成红色字体 合格变成绿色
if cell_value == "不合格":
run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00)
if cell_value == "合格":
run.font.color.rgb = RGBColor(20, 139, 0)
# 将表格的 XML 元素插入到当前段落元素的前面
paragraph._element.addprevious(table._tbl)
# 删除原来的占位段落
p_element = paragraph._element
p_element.getparent().remove(p_element)
print(f"Info:新增表格{tableName}")
break
else:
keys_list = re.findall(r'\$([a-zA-Z0-9_]+)', text)
if not keys_list:
return
for keyName in keys_list:
value = data_dict.get(keyName)
if value is None:
continue
# placeholder = f"${keyName}"
placeholder = keyName
# 尝试在Run级别替换保留格式
for run in paragraph.runs:
if "$" in run.text:
textKey = run.text.split("$")[1]
if textKey == "":
run.text = ""
elif keyName in textKey:
run.text = run.text.replace(f"${keyName}", str(value))
print(f"Info: 文本替换: {placeholder} -> {str(value)}")
elif run.text == keyName:
run.text = run.text.replace(placeholder, str(value))
print(f"Info: 文本替换: {placeholder} -> {str(value)}")
def clear_paragraph_indent(paragraph):
"""强制清除段落的所有缩进"""
pPr = paragraph._element.get_or_add_pPr()
ind = pPr.get_or_add_ind()
ind.set(qn('w:firstLine'), '0')
ind.set(qn('w:firstLineChars'), '0')
ind.set(qn('w:left'), '0')
ind.set(qn('w:leftChars'), '0')
def crop_image_to_ratio(image_path, target_width_emu, target_height_emu):
"""
根据目标宽高的比例对图片进行居中裁剪
返回: BytesIO 对象 (可直接传给 add_picture)
"""
try:
with Image.open(image_path) as img:
# 1. 计算目标宽高比
target_ratio = target_width_emu / target_height_emu
img_ratio = img.width / img.height
# 2. 计算裁剪区域 (Box: left, upper, right, lower)
if img_ratio > target_ratio:
# 图片太宽,需要裁掉左右两边
new_width = int(img.height * target_ratio)
offset = (img.width - new_width) // 2
box = (offset, 0, img.width - offset, img.height)
else:
# 图片太高,需要裁掉上下两边
new_height = int(img.width / target_ratio)
offset = (img.height - new_height) // 2
box = (0, offset, img.width, img.height - offset)
# 3. 执行裁剪
cropped_img = img.crop(box)
# 4. 保存到内存流
img_byte_arr = io.BytesIO()
# 保持原格式如果是PNG保持RGBA否则转RGB
img_format = img.format if img.format else 'PNG'
cropped_img.save(img_byte_arr, format=img_format)
img_byte_arr.seek(0)
return img_byte_arr
except Exception as e:
print(f"裁剪图片出错: {e}")
return None
def process_images_in_paragraphs(doc, image_dict, paragraphs, imgName_dict, projectPath):
"""
将所有图片包含换行部分全部放入一个统一的大表格中
"""
section = doc.sections[0]
page_content_width = section.page_width - \
section.left_margin - section.right_margin
gap_width_emu = 144000
for p in list(paragraphs):
blips = p._element.xpath('.//a:blip')
for blip in blips:
rId = blip.get(qn('r:embed'))
if rId in doc.part.rels:
rel = doc.part.rels[rId]
imageItem = rel.target_ref.split("/")[-1]
picName = image_dict.get(imageItem, None)
pic_value_list = imgName_dict.get(picName, [])
if not pic_value_list:
continue
# 1. 获取基准尺寸
original_cx, original_cy = 2743200, 2743200
extents = p._element.xpath('.//wp:extent')
if extents:
try:
original_cx = int(extents[0].get('cx'))
original_cy = int(extents[0].get('cy'))
except:
pass
# 2. 计算分行布局 (例如: [[图1,图2,图3], [图4]])
rows_data = []
current_row = []
current_w = 0
for item in pic_value_list:
if not current_row:
current_row.append(item)
current_w = original_cx
elif current_w + gap_width_emu + original_cx <= page_content_width:
current_row.append(item)
current_w += (gap_width_emu + original_cx)
else:
rows_data.append(current_row)
current_row = [item]
current_w = original_cx
if current_row:
rows_data.append(current_row)
# 3. 创建统一的大表格
# 总行数 = 计算出的行数 * 2 (一行图,一行文)
# 总列数 = 所有行中图片最多的那一行的数量
total_cols = max(len(r) for r in rows_data)
big_table = doc.add_table(
rows=len(rows_data) * 2, cols=total_cols)
p._element.addnext(big_table._element)
# 移除大表边框
tblPr = big_table._tbl.tblPr
borders = OxmlElement('w:tblBorders')
for b in ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']:
node = OxmlElement(f'w:{b}')
node.set(qn('w:val'), 'nil')
borders.append(node)
tblPr.append(borders)
# 4. 填充大表格内容
for row_idx, row_content in enumerate(rows_data):
img_row_num = row_idx * 2 # 图片所在的行
txt_row_num = row_idx * 2 + 1 # 文字所在的行
for col_idx, img_item in enumerate(row_content):
name = list(img_item.keys())[0]
title = img_item[name]
# path = os.path.join(projectPath, f"{name}.png")
path = name
# A. 插入图片
cell_img = big_table.cell(img_row_num, col_idx)
p_img = cell_img.paragraphs[0]
p_img.alignment = WD_ALIGN_PARAGRAPH.CENTER
clear_paragraph_indent(p_img)
stream = crop_image_to_ratio(
path, original_cx, original_cy)
p_img.add_run().add_picture(stream, width=Emu(original_cx), height=Emu(original_cy))
print(f"插入图片: {name} -> {title}")
# B. 插入名称
cell_txt = big_table.cell(txt_row_num, col_idx)
p_txt = cell_txt.paragraphs[0]
p_txt.alignment = WD_ALIGN_PARAGRAPH.CENTER
clear_paragraph_indent(p_txt)
run_txt = p_txt.add_run(title)
run_txt.font.size = Pt(10.5)
run_txt.font.name = '宋体'
run_txt._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
# 5. 删除占位段落并跳出 blip 循环
p._element.getparent().remove(p._element)
break
def getImgNameDict(reportContent_list):
"""获取图片名称字典"""
imgName_dict = {}
for item_dict in reportContent_list:
if item_dict["type"] == "img":
key_name = item_dict.get("key", "")
if key_name not in imgName_dict:
imgName_dict[key_name] = []
img_base64_list = item_dict.get("value", [])
if img_base64_list:
for img_base64_dict in img_base64_list:
imgTemp_dict = {}
picName = img_base64_dict.get("title", "")
picPath = img_base64_dict.get("path", "")
imgTemp_dict[picPath] = picName
imgName_dict[key_name].append(imgTemp_dict)
return imgName_dict
def generateWord(projectPath, outputDocxPath, reportContent_list):
"""根据JSON数据生成报告文件"""
print(f"Info:开始生成Word报告...")
doc = Document(outputDocxPath)
# 把base64转成图片
# img_num, imgName_dict = base64ToImg(projectPath, reportContent_list)
# print(f"Info:共转换{img_num}张图片")
# 获取图片地址
imgName_dict = getImgNameDict(reportContent_list)
img_keys = list(imgName_dict.keys())
data_dict, table_num = getDataDict(reportContent_list)
# image_dict = {"image1.png": "pic1",
# "image2.png": "pic2"
# }
image_dict = {
f"image{i+1}.png": img_keys[i] for i in range(len(img_keys))}
print(f"Info:图片分类:{img_keys}, 与模版的对应关系:{image_dict}")
# 处理段落和表格
for para in doc.paragraphs:
if "$" in para.text:
replaceParagraph(doc, para, data_dict)
# 处理图片
process_images_in_paragraphs(
doc, image_dict, doc.paragraphs, imgName_dict, projectPath)
# 保存文档
doc.save(outputDocxPath)
print(f"Info:Word文档已创建: {outputDocxPath}")
if __name__ == "__main__":
# currentPath = os.getcwd()
# projectPath = f"{currentPath}/project/generateReport"
# templatePath = f"{currentPath}/template/仿真报告标准模板_v1.0.docx"
# pythonLogPath = os.path.dirname(projectPath)
projectPath = sys.argv[1]
templatePath = sys.argv[2]
pythonLogPath = "/opt/script"
# 输出log日志
# 日志文件路径
log_fileDir = f"{pythonLogPath}/pythonLog"
if not os.path.exists(log_fileDir):
os.makedirs(log_fileDir)
# log文件用日期命名
log_file = f"{log_fileDir}/{datetime.now().strftime('%Y%m%d')}.log"
# 使用上下文管理器重定向所有输出到文件
with open(log_file, 'a', encoding='utf-8') as log_file_obj:
# 重定向标准输出和标准错误
original_stdout = sys.stdout
original_stderr = sys.stderr
# 创建Tee对象同时输出到文件和原控制台
tee = Tee(log_file_obj, original_stdout)
sys.stdout = tee
sys.stderr = tee
# 在日志开头添加时间戳
print("=" * 60)
print(f"生成报告 程序开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"日志文件: {log_file}")
print(f"项目路径: {projectPath}")
print("=" * 60)
try:
# 获取项目信息数据
reportContentPath = f"{projectPath}/reportContent.json"
reportContent_list = getJsonData(reportContentPath)
# # 判断是否批量
# isBatch = False
# if isBatch:
# projectPath = f"{projectPath}/{loacase}"
# # 是否自动出报告
# autoExport = projectInfo_dict.get("autoExport", False)
# if autoExport:
# exportWord(projectPath, templatePath, loacase=loacase)
# else:
# exportWordByUser(projectPath, templatePath, loacase=loacase)
# 拷贝模板文件到项目目录下
outputDocxPath = f"{projectPath}/report_{datetime.now().strftime('%Y%m%d')}.docx"
shutil.copy(templatePath, outputDocxPath)
generateWord(projectPath, outputDocxPath, reportContent_list)
print("\n" + "=" * 60)
print(f"程序结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("程序执行成功")
print("=" * 60)
except Exception as e:
print(f"\n程序执行出错: {str(e)}", file=sys.stderr)
traceback.print_exc()
print("=" * 60)
print(f"程序异常结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
raise
finally:
# 恢复标准输出
sys.stdout = original_stdout
sys.stderr = original_stderr