fix(ppt): 表格行高按内容估算逐行设置——显式换行强制断行+CJK/ASCII显示宽度估算换行数+行距余量1.45;根治实测长文本(含换行项目符号列表)在行底被截断;总高超页截行加提示不跨页拆表

This commit is contained in:
yumoqing 2026-09-11 17:18:58 +08:00
parent 47f191fc81
commit 09d77f7483

View File

@ -233,33 +233,65 @@ def _content_header(slide, title, kicker=""):
# ══════════════════════ 表格渲染(真 Table,非文本) ══════════════════════ # ══════════════════════ 表格渲染(真 Table,非文本) ══════════════════════
def _disp_w(s):
"""显示宽度(CJK=1.0、ASCII≈0.55 字符单位),用于估算换行数。"""
return sum(1.0 if ord(ch) > 0x2E7F else 0.55 for ch in (s or ""))
def _add_table(slide, rows, top, width=None): def _add_table(slide, rows, top, width=None):
"""渲染 markdown 表格为 pptx Table;返回占用高度(英寸)。超宽自动缩字号。""" """渲染 markdown 表格为 pptx Table;返回占用高度(英寸)。超宽自动缩字号。
行高按内容估算逐行设置(2026-09-11 实测教训:统一行高时 LibreOffice/WPS
不按换行撑行 → 长文本在行底被截断)。总高超页则截行加提示(不跨页拆表,
保持单表完整可读)。
"""
from pptx.util import Inches, Pt from pptx.util import Inches, Pt
import math
if not rows or not rows[0]: if not rows or not rows[0]:
return 0.0 return 0.0
width = width or (SW - 2 * MARGIN_X) width = width or (SW - 2 * MARGIN_X)
ncol = max(len(r) for r in rows) ncol = max(len(r) for r in rows)
rows = [r + [""] * (ncol - len(r)) for r in rows] rows = [r + [""] * (ncol - len(r)) for r in rows]
nrow = len(rows) nrow = len(rows)
row_h = 0.36
avail = SH - 0.95 - top avail = SH - 0.95 - top
max_rows = max(2, int(avail / row_h))
if nrow > max_rows:
rows = rows[:max_rows - 1] + [["… 其余 %d 行见平台详情页" % (nrow - max_rows + 1)] + [""] * (ncol - 1)]
nrow = len(rows)
total_h = min(row_h * nrow, avail)
gf = slide.shapes.add_table(nrow, ncol, Inches(MARGIN_X), Inches(top),
Inches(width), Inches(total_h))
tbl = gf.table
# 列宽:首列略宽(多为名称),其余均分 # 列宽:首列略宽(多为名称),其余均分
first = min(width * 0.30, 4.6) first = min(width * 0.30, 4.6)
rest = (width - first) / max(1, ncol - 1) rest = (width - first) / max(1, ncol - 1)
for ci in range(ncol): col_w = [first if ci == 0 else rest for ci in range(ncol)]
tbl.columns[ci].width = Inches(first if ci == 0 else rest)
fs = 11 if ncol <= 5 else (10 if ncol <= 7 else 9) fs = 11 if ncol <= 5 else (10 if ncol <= 7 else 9)
line_h = fs * 1.45 / 72.0 # 每行文本高度(英寸,含行距余量)
pad = 0.12 # 单元格上下边距+余量
def row_height(row_vals):
lines = 1
for ci, val in enumerate(row_vals):
cap = max(2.0, (col_w[ci] - 0.16) * 72.0 / fs) # 每行可容纳字符单位
n = 0
for seg in (val or "")[:180].split("\n"): # 显式换行强制断行
n += max(1, math.ceil(_disp_w(seg) / cap))
lines = max(lines, n)
return lines * line_h + pad
heights = [row_height(r) for r in rows]
total_h = sum(heights)
if total_h > avail:
# 从尾行往前砍到放得下(至少留表头+1 数据行+提示行)
keep = nrow
while keep > 3 and sum(heights[:keep]) + row_height(
["…"] + [""] * (ncol - 1)) > avail:
keep -= 1
cut = nrow - keep
rows = rows[:keep] + [["… 其余 %d 行见平台详情页" % cut] + [""] * (ncol - 1)]
heights = heights[:keep] + [row_height(rows[-1])]
nrow = len(rows)
total_h = sum(heights)
gf = slide.shapes.add_table(nrow, ncol, Inches(MARGIN_X), Inches(top),
Inches(width), Inches(total_h))
tbl = gf.table
for ci in range(ncol):
tbl.columns[ci].width = Inches(col_w[ci])
for ri, row in enumerate(rows): for ri, row in enumerate(rows):
tbl.rows[ri].height = Inches(total_h / nrow) tbl.rows[ri].height = Inches(heights[ri])
for ci, val in enumerate(row): for ci, val in enumerate(row):
cell = tbl.cell(ri, ci) cell = tbl.cell(ri, ci)
cell.margin_left = cell.margin_right = Inches(0.07) cell.margin_left = cell.margin_right = Inches(0.07)