添加签名图片处理功能

This commit is contained in:
2026-01-04 13:01:37 +08:00
parent 475d23f49e
commit 7f23d64eb2
2 changed files with 138 additions and 22 deletions

View File

@@ -239,8 +239,8 @@ class DocxWriter:
f". 课程目标达成情况的合理性评价") f". 课程目标达成情况的合理性评价")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True) self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
rows = 9 rows = 11
cols = 4 cols = 6
table = doc.add_table(rows=rows, cols=cols) table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅 # 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table) self.set_table_borders(table)
@@ -250,9 +250,21 @@ class DocxWriter:
cell_end = table.cell(0, cols - 1) cell_end = table.cell(0, cols - 1)
cell_start.merge(cell_end) cell_start.merge(cell_end)
# 合并第二行至最后 # 合并第二行至最后
for i in range(1, 9): for i in range(1, rows):
if i == 2: match i:
continue case 2:
table.cell(i, 2).merge(table.cell(i, 3))
table.cell(i, 4).merge(table.cell(i, 5))
table.cell(i, 1).width = Cm(7.42)
table.cell(i, 2).width = Cm(7.42)
table.cell(i, 4).width = Cm(7.41)
case 8 | 10:
table.cell(i - 1, 0).merge(table.cell(i, 0))
table.cell(i, 1).width = Cm(11.23)
table.cell(i, 2).width = Cm(1.48)
table.cell(i, 3).width = Cm(3.4)
table.cell(i, 4).width = Cm(1.39)
case _:
cell_start = table.cell(i, 1) cell_start = table.cell(i, 1)
cell_end = table.cell(i, cols - 1) cell_end = table.cell(i, cols - 1)
cell_start.merge(cell_end) cell_start.merge(cell_end)
@@ -276,8 +288,36 @@ class DocxWriter:
for t_index, table in enumerate(doc.tables): for t_index, table in enumerate(doc.tables):
self.set_table_borders(table) self.set_table_borders(table)
# part_3_table_index 表格第9和11行索引8和10特殊边框处理
if t_index in part_3_table_index:
for r_idx in [8, 10]:
row = table.rows[r_idx]
prev_row = table.rows[r_idx - 1]
# 上一行第8行和第10行索引7和9第2-6列移除下边框
for c_idx in range(1, 6):
self.set_cell_border(prev_row.cells[c_idx], bottom=0)
# 第2列索引1没有上边框和右边框
self.set_cell_border(row.cells[1], top=0, right=0)
# 第3-5列索引2-4没有上边框和左右边框
for c_idx in [2, 3, 4]:
self.set_cell_border(row.cells[c_idx], top=0, left=0, right=0)
# 第6列索引5没有上边框和左边框
self.set_cell_border(row.cells[5], top=0, left=0)
# 插入签名图片
if self.excel_reader.major_director_signature_image is not None:
self.insert_pil_image(table.cell(8, 3),
self.excel_reader.major_director_signature_image,
height=Cm(1.2))
if self.excel_reader.course_leader_signature_image is not None:
self.insert_pil_image(table.cell(10, 3),
self.excel_reader.course_leader_signature_image,
height=Cm(1.2))
for r_index, row in enumerate(table.rows): for r_index, row in enumerate(table.rows):
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
# part_3_table_index 表格第9和11行索引8和10行高为1.2cm
if t_index in part_3_table_index and r_index in [8, 10]:
row.height = Cm(1.2)
else:
row.height = Cm(0.7) row.height = Cm(0.7)
for c_index, cell in enumerate(row.cells): for c_index, cell in enumerate(row.cells):
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
@@ -336,6 +376,8 @@ class DocxWriter:
(6, 1), (6, 1),
(7, 1), (7, 1),
(8, 1), (8, 1),
(9, 1),
(10, 1),
] ]
if r_index == 0: if r_index == 0:
for run in paragraph.runs: for run in paragraph.runs:
@@ -379,6 +421,7 @@ class DocxWriter:
""" """
设置单元格边框 设置单元格边框
kwargs: top, bottom, left, right, inside_h, inside_v kwargs: top, bottom, left, right, inside_h, inside_v
值为0时移除边框值大于0时设置边框粗细
""" """
tc = cell._tc tc = cell._tc
tcPr = tc.get_or_add_tcPr() tcPr = tc.get_or_add_tcPr()
@@ -391,6 +434,10 @@ class DocxWriter:
if value is not None: if value is not None:
tag = 'w:{}'.format(key) tag = 'w:{}'.format(key)
border = OxmlElement(tag) border = OxmlElement(tag)
if value == 0:
# 移除边框
border.set(qn('w:val'), 'nil')
else:
border.set(qn('w:val'), 'single') border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), str(int(value * 8))) border.set(qn('w:sz'), str(int(value * 8)))
border.set(qn('w:space'), '0') border.set(qn('w:space'), '0')
@@ -490,6 +537,15 @@ class DocxWriter:
run = paragraph.add_run() run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height) run.add_picture(image_stream, width=width, height=height)
def insert_pil_image(self, cell, pil_image, width=None, height=Cm(4.5)):
"""插入PIL Image对象到单元格"""
image_stream = io.BytesIO()
pil_image.save(image_stream, format='PNG')
image_stream.seek(0)
paragraph = cell.paragraphs[0]
run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height)
def is_chinese(self, char): def is_chinese(self, char):
"""判断字符是否为中文""" """判断字符是否为中文"""
if '\u4e00' <= char <= '\u9fff': if '\u4e00' <= char <= '\u9fff':

View File

@@ -15,6 +15,7 @@
import datetime import datetime
import traceback import traceback
import io
from typing import Optional, Callable from typing import Optional, Callable
import openpyxl import openpyxl
@@ -22,6 +23,7 @@ from openpyxl.utils import get_column_letter, column_index_from_string
from openpyxl.workbook.workbook import Workbook from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from packaging import version from packaging import version
from PIL import Image
from module import LOGLEVEL, COMPATIBLE_VERSION from module import LOGLEVEL, COMPATIBLE_VERSION
from module.schema import Performance from module.schema import Performance
@@ -49,6 +51,27 @@ class ExcelReader:
ignore_version_check: bool ignore_version_check: bool
pic_list: list pic_list: list
suggestion_template_list: list[Optional[str]] suggestion_template_list: list[Optional[str]]
major_director_signature_image: Optional[Image.Image]
course_leader_signature_image: Optional[Image.Image]
class _SheetImageLoader:
"""Lightweight image loader scoped for ExcelReader use."""
def __init__(self, sheet: Worksheet):
self._images: dict[str, Callable[[], bytes]] = {}
for image in getattr(sheet, "_images", []):
row = image.anchor._from.row + 1
col = get_column_letter(image.anchor._from.col + 1)
self._images[f"{col}{row}"] = image._data
def image_in(self, cell: str) -> bool:
return cell in self._images
def get(self, cell: str) -> Image.Image:
if cell not in self._images:
raise ValueError(f"Cell {cell} doesn't contain an image")
image = io.BytesIO(self._images[cell]())
return Image.open(image)
def __init__(self, file_path: str, version_check: bool = False, def __init__(self, file_path: str, version_check: bool = False,
signal: Callable[[str, str], None] = lambda x, y: print(x)): signal: Callable[[str, str], None] = lambda x, y: print(x)):
@@ -75,6 +98,8 @@ class ExcelReader:
self.pic_list = [] self.pic_list = []
self.signal = signal self.signal = signal
self.suggestion_template_list = [] self.suggestion_template_list = []
self.major_director_signature_image = None
self.course_leader_signature_image = None
def parse_excel(self): def parse_excel(self):
try: try:
@@ -104,6 +129,13 @@ class ExcelReader:
# 读取课程负责人 # 读取课程负责人
self.course_lead_teacher_name = sheet["D8"].value self.course_lead_teacher_name = sheet["D8"].value
need_signature_images = CUR_VERSION >= version.parse("9.4") and sheet["H10"].value == ""
if need_signature_images:
self._load_signature_images()
else:
self.major_director_signature_image = None
self.course_leader_signature_image = None
# 读取班级和人数 # 读取班级和人数
max_class_size = 4 max_class_size = 4
match CUR_VERSION: match CUR_VERSION:
@@ -259,6 +291,24 @@ class ExcelReader:
def run(self): def run(self):
self.parse_excel() self.parse_excel()
def _load_signature_images(self):
signature_cells = {
"major_director_signature_image": "K34",
"course_leader_signature_image": "K35",
}
wb_with_images: Workbook = openpyxl.load_workbook(self.file_path, data_only=True)
try:
sheet_with_images: Worksheet = wb_with_images["初始录入"]
loader = self._SheetImageLoader(sheet_with_images)
for attr, cell in signature_cells.items():
if loader.image_in(cell):
setattr(self, attr, loader.get(cell))
else:
setattr(self, attr, None)
finally:
wb_with_images.close()
def clear_all_data(self): def clear_all_data(self):
self.kpi_list = [] self.kpi_list = []
self.kpi_number = 0 self.kpi_number = 0
@@ -278,6 +328,8 @@ class ExcelReader:
self.hml_list = [] self.hml_list = []
self.question_data = {} self.question_data = {}
self.pic_list = [] self.pic_list = []
self.major_director_signature_image = None
self.course_leader_signature_image = None
def set_version_check(self, version_check: bool): def set_version_check(self, version_check: bool):
self.ignore_version_check = version_check self.ignore_version_check = version_check
@@ -486,7 +538,7 @@ class ExcelReader:
yield "改进措施" yield "改进措施"
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}") f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
for i in range(88888): while True:
yield "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"
def get_word_template_part_2(self): def get_word_template_part_2(self):
@@ -581,7 +633,7 @@ class ExcelReader:
yield "改进措施" yield "改进措施"
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n" yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}") f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
for i in range(88888): while True:
yield "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"
def get_word_template_part_3(self): def get_word_template_part_3(self):
@@ -603,14 +655,22 @@ class ExcelReader:
yield "专业负责人/系主任(签字)" yield "专业负责人/系主任(签字)"
yield ("整改意见:\n" yield ("整改意见:\n"
"\n\n\n" "\n\n\n"
f"{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}" f"{" "*8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n")
f"\n\n\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{datetime.datetime.now().strftime("%Y-%m-%d")}\n") yield ""
yield "签字:"
yield ""
yield "日期:"
yield datetime.datetime.now().strftime("%Y-%m-%d")
yield "课程负责人(签字)" yield "课程负责人(签字)"
yield ("拟整改计划与措施:\n" yield ("拟整改计划与措施:\n"
"\n\n\n" "\n\n\n"
f"{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}" f"{" "*8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n")
f"\n\n\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{datetime.datetime.now().strftime("%Y-%m-%d")}\n") yield ""
for i in range(88888): yield "签字:"
yield ""
yield "日期:"
yield datetime.datetime.now().strftime("%Y-%m-%d")
while True:
yield "如果您看到了本段文字,请联系开发者" yield "如果您看到了本段文字,请联系开发者"