diff --git a/pipeline_opportunity/opp_ppt.py b/pipeline_opportunity/opp_ppt.py index d562d82..ba3ef05 100644 --- a/pipeline_opportunity/opp_ppt.py +++ b/pipeline_opportunity/opp_ppt.py @@ -233,33 +233,65 @@ def _content_header(slide, title, kicker=""): # ══════════════════════ 表格渲染(真 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): - """渲染 markdown 表格为 pptx Table;返回占用高度(英寸)。超宽自动缩字号。""" + """渲染 markdown 表格为 pptx Table;返回占用高度(英寸)。超宽自动缩字号。 + + 行高按内容估算逐行设置(2026-09-11 实测教训:统一行高时 LibreOffice/WPS + 不按换行撑行 → 长文本在行底被截断)。总高超页则截行加提示(不跨页拆表, + 保持单表完整可读)。 + """ from pptx.util import Inches, Pt + import math if not rows or not rows[0]: return 0.0 width = width or (SW - 2 * MARGIN_X) ncol = max(len(r) for r in rows) rows = [r + [""] * (ncol - len(r)) for r in rows] nrow = len(rows) - row_h = 0.36 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) rest = (width - first) / max(1, ncol - 1) - for ci in range(ncol): - tbl.columns[ci].width = Inches(first if ci == 0 else rest) + col_w = [first if ci == 0 else rest for ci in range(ncol)] 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): - tbl.rows[ri].height = Inches(total_h / nrow) + tbl.rows[ri].height = Inches(heights[ri]) for ci, val in enumerate(row): cell = tbl.cell(ri, ci) cell.margin_left = cell.margin_right = Inches(0.07)