Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:
- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
- docx family: kept docx (official, full toolchain) + docx-cn
(GB/T 9704 Chinese official-document constants),
dropped docx_writer (no scripts, name collided with docx)
- humanizer family: kept humanizer-zh (6 zh reference docs),
dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
live credentials
Result: 25 skills, 572 files, ~7.5 MB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
536 lines
19 KiB
Python
536 lines
19 KiB
Python
"""绘图辅助函数 — 在 slide 上添加文本、形状、表格、图片、页眉/页脚"""
|
||
|
||
import io
|
||
import re
|
||
from pathlib import Path
|
||
from urllib.request import urlopen, Request
|
||
import base64
|
||
|
||
from PIL import Image as PILImage
|
||
from pptx.util import Inches, Pt, Emu
|
||
from pptx.dml.color import RGBColor
|
||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||
from pptx.enum.shapes import MSO_SHAPE
|
||
|
||
|
||
def _rgb(hex_str: str) -> RGBColor:
|
||
"""'1E2761' -> RGBColor"""
|
||
return RGBColor.from_string(hex_str)
|
||
|
||
|
||
def set_slide_bg(slide, color_hex: str):
|
||
"""设置幻灯片纯色背景"""
|
||
bg = slide.background
|
||
fill = bg.fill
|
||
fill.solid()
|
||
fill.fore_color.rgb = _rgb(color_hex)
|
||
|
||
|
||
def add_text(slide, text: str, x, y, w, h, *,
|
||
font_size=14, font_name="微软雅黑", color="212121",
|
||
bold=False, italic=False,
|
||
align="left", valign="top",
|
||
word_wrap=True, margin_pt=None):
|
||
"""在 slide 上添加文本框,返回 shape"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
width = Inches(w) if isinstance(w, (int, float)) else w
|
||
height = Inches(h) if isinstance(h, (int, float)) else h
|
||
|
||
txBox = slide.shapes.add_textbox(left, top, width, height)
|
||
tf = txBox.text_frame
|
||
tf.word_wrap = word_wrap
|
||
|
||
if margin_pt is not None:
|
||
tf.margin_left = Pt(margin_pt)
|
||
tf.margin_right = Pt(margin_pt)
|
||
tf.margin_top = Pt(margin_pt)
|
||
tf.margin_bottom = Pt(margin_pt)
|
||
|
||
# 段落
|
||
p = tf.paragraphs[0]
|
||
p.text = text
|
||
|
||
align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}
|
||
p.alignment = align_map.get(align, PP_ALIGN.LEFT)
|
||
|
||
# 字体
|
||
run = p.runs[0] if p.runs else p.add_run()
|
||
if not p.runs:
|
||
run.text = text
|
||
run.font.size = Pt(font_size)
|
||
run.font.name = font_name
|
||
run.font.color.rgb = _rgb(color)
|
||
run.font.bold = bold
|
||
run.font.italic = italic
|
||
|
||
# 垂直对齐
|
||
from pptx.oxml.ns import qn
|
||
bodyPr = tf._txBody.find(qn("a:bodyPr"))
|
||
if bodyPr is not None:
|
||
anchor_map = {"top": "t", "middle": "ctr", "bottom": "b"}
|
||
bodyPr.set("anchor", anchor_map.get(valign, "t"))
|
||
|
||
return txBox
|
||
|
||
|
||
def add_rich_text(slide, parts, x, y, w, h, *,
|
||
font_name="微软雅黑", align="left", valign="top",
|
||
line_spacing=1.2, margin_pt=None):
|
||
"""添加富文本框 — parts 为 [{"text": ..., "size": ..., "color": ..., "bold": ...}, ...]
|
||
支持 newline=True 换行"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
width = Inches(w) if isinstance(w, (int, float)) else w
|
||
height = Inches(h) if isinstance(h, (int, float)) else h
|
||
|
||
txBox = slide.shapes.add_textbox(left, top, width, height)
|
||
tf = txBox.text_frame
|
||
tf.word_wrap = True
|
||
|
||
if margin_pt is not None:
|
||
tf.margin_left = Pt(margin_pt)
|
||
tf.margin_right = Pt(margin_pt)
|
||
tf.margin_top = Pt(margin_pt)
|
||
tf.margin_bottom = Pt(margin_pt)
|
||
|
||
align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}
|
||
|
||
p = tf.paragraphs[0]
|
||
p.alignment = align_map.get(align, PP_ALIGN.LEFT)
|
||
|
||
for i, part in enumerate(parts):
|
||
if part.get("newline") and i > 0:
|
||
p = tf.add_paragraph()
|
||
p.alignment = align_map.get(align, PP_ALIGN.LEFT)
|
||
if line_spacing != 1.0:
|
||
p.line_spacing = Pt(int(part.get("size", 14) * line_spacing))
|
||
|
||
run = p.add_run()
|
||
run.text = part["text"]
|
||
run.font.size = Pt(part.get("size", 14))
|
||
run.font.name = part.get("font", font_name)
|
||
run.font.color.rgb = _rgb(part.get("color", "212121"))
|
||
run.font.bold = part.get("bold", False)
|
||
run.font.italic = part.get("italic", False)
|
||
|
||
return txBox
|
||
|
||
|
||
def add_shape(slide, shape_type, x, y, w, h, *,
|
||
fill=None, line_color=None, line_width=None):
|
||
"""添加形状"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
width = Inches(w) if isinstance(w, (int, float)) else w
|
||
height = Inches(h) if isinstance(h, (int, float)) else h
|
||
|
||
shape = slide.shapes.add_shape(shape_type, left, top, width, height)
|
||
|
||
if fill:
|
||
shape.fill.solid()
|
||
shape.fill.fore_color.rgb = _rgb(fill)
|
||
else:
|
||
shape.fill.background()
|
||
|
||
if line_color:
|
||
shape.line.color.rgb = _rgb(line_color)
|
||
shape.line.width = Pt(line_width or 1)
|
||
else:
|
||
shape.line.fill.background()
|
||
|
||
return shape
|
||
|
||
|
||
def add_rect(slide, x, y, w, h, **kwargs):
|
||
return add_shape(slide, MSO_SHAPE.RECTANGLE, x, y, w, h, **kwargs)
|
||
|
||
|
||
def add_rounded_rect(slide, x, y, w, h, **kwargs):
|
||
return add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h, **kwargs)
|
||
|
||
|
||
def add_line(slide, x, y, w, *, color="E0E0E0", width=1):
|
||
"""添加水平线"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
w_emu = Inches(w) if isinstance(w, (int, float)) else w
|
||
|
||
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, w_emu, Pt(width))
|
||
shape.fill.solid()
|
||
shape.fill.fore_color.rgb = _rgb(color)
|
||
shape.line.fill.background()
|
||
return shape
|
||
|
||
|
||
# ── Picture ────────────────────────────────────────────────────────────
|
||
|
||
def _resolve_image_source(image_src):
|
||
"""将图片来源统一为 (BytesIO stream, (width, height)) 元组
|
||
|
||
支持三种来源:
|
||
- 本地文件路径 (str / Path)
|
||
- URL (http:// / https://)
|
||
- base64 字符串 ("data:image/...;base64,..." 或裸 base64)
|
||
|
||
返回 (stream, (img_w, img_h))。stream 可直接传给 python-pptx。
|
||
"""
|
||
src = str(image_src)
|
||
|
||
if src.startswith(("http://", "https://")):
|
||
req = Request(src, headers={"User-Agent": "ppt-station/1.0"})
|
||
resp = urlopen(req, timeout=30)
|
||
raw_bytes = resp.read()
|
||
elif src.startswith("data:"):
|
||
# data URI: "data:image/png;base64,iVBOR..."
|
||
b64_data = src.split(";base64,", 1)[1]
|
||
raw_bytes = base64.b64decode(b64_data)
|
||
else:
|
||
# 先尝试本地文件,不存在则当作裸 base64
|
||
p = Path(src).expanduser().resolve()
|
||
if p.exists():
|
||
raw_bytes = p.read_bytes()
|
||
else:
|
||
try:
|
||
raw_bytes = base64.b64decode(src)
|
||
except Exception:
|
||
raise FileNotFoundError(f"图片文件不存在: {p}")
|
||
|
||
# 用独立 BytesIO 读尺寸,避免 PIL 关闭影响主 stream
|
||
with PILImage.open(io.BytesIO(raw_bytes)) as pil:
|
||
size = pil.size
|
||
|
||
return io.BytesIO(raw_bytes), size
|
||
|
||
|
||
def add_picture(slide, image_src, x, y, w, h, *, sizing="stretch"):
|
||
"""在 slide 上插入图片,返回 Picture shape
|
||
|
||
Args:
|
||
image_src: 图片来源 — 本地路径 / URL / base64 字符串
|
||
x, y, w, h: 目标区域(英寸或 Emu)
|
||
sizing:
|
||
"stretch" — 拉伸填满目标区域(可能变形)
|
||
"contain" — 等比缩放适配,居中显示(可能留白)
|
||
"cover" — 等比缩放裁切填满(无变形无留白)
|
||
"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
width = Inches(w) if isinstance(w, (int, float)) else w
|
||
height = Inches(h) if isinstance(h, (int, float)) else h
|
||
|
||
stream, (img_w, img_h) = _resolve_image_source(image_src)
|
||
|
||
if sizing == "stretch":
|
||
return slide.shapes.add_picture(stream, left, top, width, height)
|
||
|
||
target_ratio = w / h if isinstance(w, (int, float)) and isinstance(h, (int, float)) else (width / height)
|
||
img_ratio = img_w / img_h
|
||
|
||
if sizing == "contain":
|
||
if img_ratio > target_ratio:
|
||
# 图片更宽 → 宽度撑满,高度居中
|
||
final_w = w if isinstance(w, (int, float)) else width
|
||
final_h = (w / img_ratio) if isinstance(w, (int, float)) else int(width / img_ratio)
|
||
else:
|
||
# 图片更高 → 高度撑满,宽度居中
|
||
final_h = h if isinstance(h, (int, float)) else height
|
||
final_w = (h * img_ratio) if isinstance(h, (int, float)) else int(height * img_ratio)
|
||
|
||
if isinstance(w, (int, float)):
|
||
offset_x = x + (w - final_w) / 2
|
||
offset_y = y + (h - final_h) / 2
|
||
return slide.shapes.add_picture(
|
||
stream,
|
||
Inches(offset_x), Inches(offset_y),
|
||
Inches(final_w), Inches(final_h))
|
||
else:
|
||
offset_x = left + (width - final_w) // 2
|
||
offset_y = top + (height - final_h) // 2
|
||
return slide.shapes.add_picture(stream, offset_x, offset_y, final_w, final_h)
|
||
|
||
if sizing == "cover":
|
||
# 先拉伸填满目标区域
|
||
pic = slide.shapes.add_picture(stream, left, top, width, height)
|
||
# 通过 srcRect crop 恢复宽高比
|
||
if img_ratio > target_ratio:
|
||
# 图片更宽 → 裁左右
|
||
crop = (1 - target_ratio / img_ratio) / 2
|
||
_set_crop(pic, left=crop, right=crop)
|
||
elif img_ratio < target_ratio:
|
||
# 图片更高 → 裁上下
|
||
crop = (1 - img_ratio / target_ratio) / 2
|
||
_set_crop(pic, top=crop, bottom=crop)
|
||
return pic
|
||
|
||
raise ValueError(f"未知 sizing 模式: {sizing}")
|
||
|
||
|
||
def _set_crop(pic, *, left=0, right=0, top=0, bottom=0):
|
||
"""通过 OOXML srcRect 设置图片裁切"""
|
||
from pptx.oxml.ns import qn
|
||
from lxml import etree
|
||
|
||
blipFill = pic._element.find(qn("p:blipFill"))
|
||
if blipFill is None:
|
||
return
|
||
|
||
srcRect = blipFill.find(qn("a:srcRect"))
|
||
if srcRect is None:
|
||
srcRect = etree.SubElement(blipFill, qn("a:srcRect"))
|
||
|
||
# OOXML srcRect 用千分比(1000 = 1%,100000 = 100%)
|
||
if left:
|
||
srcRect.set("l", str(int(left * 100000)))
|
||
if right:
|
||
srcRect.set("r", str(int(right * 100000)))
|
||
if top:
|
||
srcRect.set("t", str(int(top * 100000)))
|
||
if bottom:
|
||
srcRect.set("b", str(int(bottom * 100000)))
|
||
|
||
|
||
# ── Content Page Setup ─────────────────────────────────────────────────
|
||
|
||
def setup_content_page(slide, data, theme):
|
||
"""内容页通用初始化:背景 + 页眉标题
|
||
|
||
返回常用布局参数元组 (m, sw, content_y, content_w, footer_y)。
|
||
"""
|
||
m = theme["margin"]
|
||
sw = theme.get("slide_w", 13.333)
|
||
content_y = theme.get("content_y", 1.00)
|
||
content_w = theme.get("content_w", sw - 2 * m)
|
||
footer_y = theme.get("footer_y", 7.10)
|
||
|
||
set_slide_bg(slide, theme["bg_light"])
|
||
add_page_header(slide, data["title"], theme)
|
||
|
||
return m, sw, content_y, content_w, footer_y
|
||
|
||
|
||
# ── Page Header / Footer (投行级标准) ──────────────────────────────
|
||
|
||
def add_page_header(slide, title, theme):
|
||
"""投行级标准页眉: 16pt 标题 + 全宽分隔线
|
||
|
||
使用 theme tokens: page_title_size, header_y, divider_y, divider_h, content_w
|
||
"""
|
||
m = theme["margin"]
|
||
sw = theme.get("slide_w", 13.333)
|
||
page_title_size = theme.get("page_title_size", 16)
|
||
header_y = theme.get("header_y", 0.25)
|
||
divider_y = theme.get("divider_y", 0.80)
|
||
divider_h = theme.get("divider_h", 0.015)
|
||
content_w = theme.get("content_w", sw - 2 * m)
|
||
|
||
# 页面标题
|
||
add_text(slide, title,
|
||
x=m, y=header_y, w=content_w, h=0.5,
|
||
font_size=page_title_size, color=theme["text_dark"],
|
||
bold=True, font_name=theme["header_font"])
|
||
|
||
# 全宽分隔线
|
||
add_rect(slide, m, divider_y, content_w, divider_h, fill=theme["primary"])
|
||
|
||
|
||
def add_page_footer(slide, theme, source="", page_num=None, brand=""):
|
||
"""投行级标准页脚: 左侧来源 8pt + 右侧页码/品牌
|
||
|
||
使用 theme tokens: footer_size, footer_y, content_w
|
||
"""
|
||
m = theme["margin"]
|
||
sw = theme.get("slide_w", 13.333)
|
||
footer_size = theme.get("footer_size", 8)
|
||
footer_y = theme.get("footer_y", 7.10)
|
||
content_w = theme.get("content_w", sw - 2 * m)
|
||
|
||
# 页脚分隔细线
|
||
add_rect(slide, m, footer_y - 0.08, content_w, 0.005, fill=theme["border"])
|
||
|
||
# 左侧: 来源
|
||
if source:
|
||
source_text = f"Source: {source}" if not source.startswith("Source") else source
|
||
add_text(slide, source_text,
|
||
x=m, y=footer_y, w=content_w * 0.7, h=0.25,
|
||
font_size=footer_size, color=theme["text_muted"],
|
||
italic=True, font_name=theme["body_font"])
|
||
|
||
# 右侧: 页码/品牌
|
||
right_parts = []
|
||
if brand:
|
||
right_parts.append(brand)
|
||
if page_num is not None:
|
||
right_parts.append(str(page_num))
|
||
if right_parts:
|
||
add_text(slide, " | ".join(right_parts),
|
||
x=m + content_w * 0.3, y=footer_y, w=content_w * 0.7, h=0.25,
|
||
font_size=footer_size, color=theme["text_muted"],
|
||
align="right", font_name=theme["body_font"])
|
||
|
||
|
||
# ── Table (投行级) ──────────────────────────────────────────────────────
|
||
|
||
_NUM_PATTERN = re.compile(r'^[\s]*[-+]?[\d,]+\.?\d*\s*[%xX倍万亿]?\s*$')
|
||
|
||
|
||
def _is_numeric_cell(val: str) -> bool:
|
||
"""检测单元格值是否为数值型(用于右对齐)"""
|
||
if not val:
|
||
return False
|
||
return bool(_NUM_PATTERN.match(val))
|
||
|
||
|
||
def add_table(slide, headers, rows, x, y, w, h, *,
|
||
theme=None, header_bg=None, header_color="FFFFFF",
|
||
font_name="微软雅黑", font_size=11,
|
||
highlight_row=None, highlight_bg="EBF5FF",
|
||
highlight_cols=None,
|
||
ib_style=True):
|
||
"""添加带样式的表格
|
||
|
||
Args:
|
||
highlight_cols: 高亮列索引列表 (如 [2, 3])
|
||
ib_style: True 启用投行级样式(无垂直边框、数值右对齐、斑马纹 token)
|
||
"""
|
||
left = Inches(x) if isinstance(x, (int, float)) else x
|
||
top = Inches(y) if isinstance(y, (int, float)) else y
|
||
width = Inches(w) if isinstance(w, (int, float)) else w
|
||
height = Inches(h) if isinstance(h, (int, float)) else h
|
||
|
||
n_rows = len(rows) + 1
|
||
n_cols = len(headers)
|
||
|
||
table_shape = slide.shapes.add_table(n_rows, n_cols, left, top, width, height)
|
||
table = table_shape.table
|
||
|
||
# 均匀列宽
|
||
col_w = int(width / n_cols)
|
||
for j in range(n_cols):
|
||
table.columns[j].width = col_w
|
||
|
||
# 颜色 tokens
|
||
hdr_bg = header_bg or (theme.get("table_header_bg", theme["primary"]) if theme else "36454F")
|
||
zebra_even = theme.get("table_zebra_even", "F8FAFC") if theme else "F8FAFC"
|
||
zebra_odd = theme.get("table_zebra_odd", "FFFFFF") if theme else "FFFFFF"
|
||
highlight_col_bg = theme.get("table_zebra_even", "EBF5FF") if theme else "EBF5FF"
|
||
|
||
# 预扫描: 检测哪些列是数值列(用于自动右对齐)
|
||
numeric_cols = set()
|
||
if ib_style and rows:
|
||
for j in range(n_cols):
|
||
num_count = sum(1 for row in rows if len(row) > j and _is_numeric_cell(str(row[j] or "")))
|
||
if num_count > len(rows) * 0.5:
|
||
numeric_cols.add(j)
|
||
|
||
# 表头
|
||
for j, hdr in enumerate(headers):
|
||
cell = table.cell(0, j)
|
||
cell.text = str(hdr)
|
||
align = "right" if (ib_style and j in numeric_cols) else "center"
|
||
_style_cell(cell, bg=hdr_bg, color=header_color,
|
||
font_name=font_name, font_size=font_size, bold=True,
|
||
align=align)
|
||
|
||
# 数据行
|
||
for i, row in enumerate(rows):
|
||
is_highlight = (highlight_row is not None and i == highlight_row)
|
||
for j, val in enumerate(row):
|
||
cell = table.cell(i + 1, j)
|
||
cell_text = str(val) if val is not None else ""
|
||
cell.text = cell_text
|
||
|
||
# 背景色优先级: highlight_row > highlight_cols > zebra
|
||
if is_highlight:
|
||
bg = highlight_bg
|
||
elif highlight_cols and j in highlight_cols:
|
||
bg = highlight_col_bg
|
||
else:
|
||
bg = zebra_even if i % 2 == 0 else zebra_odd
|
||
|
||
# 对齐: 数值列右对齐, 首列左对齐, 其余居中
|
||
if ib_style and j in numeric_cols:
|
||
align = "right"
|
||
elif j == 0:
|
||
align = "left"
|
||
else:
|
||
align = "center"
|
||
|
||
_style_cell(cell, bg=bg, color="212121",
|
||
font_name=font_name, font_size=font_size,
|
||
bold=is_highlight,
|
||
align=align)
|
||
|
||
# 投行级: 移除垂直边框,仅保留水平线
|
||
if ib_style:
|
||
_remove_vertical_borders(table, n_rows, n_cols, hdr_bg)
|
||
|
||
return table_shape
|
||
|
||
|
||
def _remove_vertical_borders(table, n_rows, n_cols, header_color):
|
||
"""移除垂直边框,仅保留表头上下 + 表格底部水平线"""
|
||
from pptx.oxml.ns import qn
|
||
from lxml import etree
|
||
|
||
for i in range(n_rows):
|
||
for j in range(n_cols):
|
||
cell = table.cell(i, j)
|
||
tc = cell._tc
|
||
tcPr = tc.find(qn("a:tcPr"))
|
||
if tcPr is None:
|
||
tcPr = etree.SubElement(tc, qn("a:tcPr"))
|
||
|
||
# 清除左右边框
|
||
for border_name in ["lnL", "lnR"]:
|
||
border = tcPr.find(qn(f"a:{border_name}"))
|
||
if border is not None:
|
||
tcPr.remove(border)
|
||
border = etree.SubElement(tcPr, qn(f"a:{border_name}"))
|
||
border.set("w", "0")
|
||
noFill = etree.SubElement(border, qn("a:noFill"))
|
||
|
||
# 设置水平线: 表头底部 + 表格底部用主色细线,其余极浅
|
||
for border_name in ["lnT", "lnB"]:
|
||
border = tcPr.find(qn(f"a:{border_name}"))
|
||
if border is not None:
|
||
tcPr.remove(border)
|
||
|
||
is_header_bottom = (i == 0 and border_name == "lnB")
|
||
is_table_bottom = (i == n_rows - 1 and border_name == "lnB")
|
||
is_header_top = (i == 0 and border_name == "lnT")
|
||
|
||
border = etree.SubElement(tcPr, qn(f"a:{border_name}"))
|
||
if is_header_top or is_header_bottom or is_table_bottom:
|
||
border.set("w", "12700") # 1pt
|
||
solidFill = etree.SubElement(border, qn("a:solidFill"))
|
||
srgbClr = etree.SubElement(solidFill, qn("a:srgbClr"))
|
||
srgbClr.set("val", header_color)
|
||
else:
|
||
border.set("w", "6350") # 0.5pt
|
||
solidFill = etree.SubElement(border, qn("a:solidFill"))
|
||
srgbClr = etree.SubElement(solidFill, qn("a:srgbClr"))
|
||
srgbClr.set("val", "E2E8F0")
|
||
|
||
|
||
def _style_cell(cell, *, bg, color, font_name, font_size, bold=False, align="left"):
|
||
"""设置单元格样式"""
|
||
cell.fill.solid()
|
||
cell.fill.fore_color.rgb = _rgb(bg)
|
||
|
||
# 内边距
|
||
cell.margin_left = Pt(6)
|
||
cell.margin_right = Pt(6)
|
||
cell.margin_top = Pt(4)
|
||
cell.margin_bottom = Pt(4)
|
||
|
||
align_map = {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER, "right": PP_ALIGN.RIGHT}
|
||
|
||
for paragraph in cell.text_frame.paragraphs:
|
||
paragraph.alignment = align_map.get(align, PP_ALIGN.LEFT)
|
||
for run in paragraph.runs:
|
||
run.font.size = Pt(font_size)
|
||
run.font.name = font_name
|
||
run.font.color.rgb = _rgb(color)
|
||
run.font.bold = bold
|