dsh-courseware 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/cordis.patch.yml +9 -0
- package/engine/pptxgen/__init__.py +8 -0
- package/engine/pptxgen/__main__.py +4 -0
- package/engine/pptxgen/builtin_template.py +218 -0
- package/engine/pptxgen/cli.py +575 -0
- package/engine/pptxgen/examples.py +290 -0
- package/engine/pptxgen/imagegen.py +564 -0
- package/engine/pptxgen/layoutmap.py +211 -0
- package/engine/pptxgen/patterns.py +106 -0
- package/engine/pptxgen/preview.py +126 -0
- package/engine/pptxgen/profile.py +456 -0
- package/engine/pptxgen/render.py +1673 -0
- package/engine/pptxgen/review.py +184 -0
- package/engine/pptxgen/schemacheck.py +122 -0
- package/engine/pptxgen/schemas/XAdES.xsd +466 -0
- package/engine/pptxgen/schemas/XAdESv141.xsd +15 -0
- package/engine/pptxgen/schemas/chartEx.xsd +838 -0
- package/engine/pptxgen/schemas/dml-chart.xsd +1499 -0
- package/engine/pptxgen/schemas/dml-chartDrawing.xsd +146 -0
- package/engine/pptxgen/schemas/dml-diagram.xsd +1085 -0
- package/engine/pptxgen/schemas/dml-drawing.xsd +63 -0
- package/engine/pptxgen/schemas/dml-lockedCanvas.xsd +11 -0
- package/engine/pptxgen/schemas/dml-main.xsd +3081 -0
- package/engine/pptxgen/schemas/dml-picture.xsd +23 -0
- package/engine/pptxgen/schemas/dml-spreadsheetDrawing.xsd +185 -0
- package/engine/pptxgen/schemas/dml-wordprocessingDrawing.xsd +287 -0
- package/engine/pptxgen/schemas/drawing-chart2012.xsd +129 -0
- package/engine/pptxgen/schemas/markup-compatibility.xsd +95 -0
- package/engine/pptxgen/schemas/opc-digSig.xsd +49 -0
- package/engine/pptxgen/schemas/opc-relationships.xsd +33 -0
- package/engine/pptxgen/schemas/pml.xsd +1676 -0
- package/engine/pptxgen/schemas/shared-additionalCharacteristics.xsd +28 -0
- package/engine/pptxgen/schemas/shared-bibliography.xsd +144 -0
- package/engine/pptxgen/schemas/shared-commonSimpleTypes.xsd +172 -0
- package/engine/pptxgen/schemas/shared-customXmlDataProperties.xsd +25 -0
- package/engine/pptxgen/schemas/shared-customXmlSchemaProperties.xsd +18 -0
- package/engine/pptxgen/schemas/shared-documentPropertiesCustom.xsd +59 -0
- package/engine/pptxgen/schemas/shared-documentPropertiesExtended.xsd +56 -0
- package/engine/pptxgen/schemas/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/engine/pptxgen/schemas/shared-math.xsd +582 -0
- package/engine/pptxgen/schemas/shared-relationshipReference.xsd +25 -0
- package/engine/pptxgen/schemas/signatureInfo.xsd +103 -0
- package/engine/pptxgen/schemas/sml.xsd +4439 -0
- package/engine/pptxgen/schemas/visio.xsd +829 -0
- package/engine/pptxgen/schemas/vml-main.xsd +570 -0
- package/engine/pptxgen/schemas/vml-officeDrawing.xsd +509 -0
- package/engine/pptxgen/schemas/vml-presentationDrawing.xsd +12 -0
- package/engine/pptxgen/schemas/vml-spreadsheetDrawing.xsd +108 -0
- package/engine/pptxgen/schemas/vml-wordprocessingDrawing.xsd +96 -0
- package/engine/pptxgen/schemas/vmlDrawing.xsd +36 -0
- package/engine/pptxgen/schemas/wml.xsd +3643 -0
- package/engine/pptxgen/schemas/word12.xsd +66 -0
- package/engine/pptxgen/schemas/xlThreadedComments.xsd +59 -0
- package/engine/pptxgen/schemas/xlThreadedComments2.xsd +22 -0
- package/engine/pptxgen/schemas/xmldsig-core-schema.xsd +318 -0
- package/engine/pptxgen/shapes.py +417 -0
- package/engine/pptxgen/spec.py +529 -0
- package/engine/pptxgen/template.py +834 -0
- package/engine/pptxgen/util.py +218 -0
- package/engine/pptxgen/xmlutil.py +298 -0
- package/engine/pptxgen.sh +7 -0
- package/engine/requirements.txt +9 -0
- package/lib/index.js +186 -0
- package/package.json +34 -0
- package/skills/courseware/SKILL.md +130 -0
- package/skills/courseware/references/content-polish.md +121 -0
- package/skills/courseware/references/deck-spec.md +155 -0
- package/skills/courseware/references/design-review.md +121 -0
- package/skills/courseware/references/image-backends.md +94 -0
- package/skills/courseware/references/quality-gates.md +71 -0
- package/skills/courseware/references/roadmap.md +55 -0
- package/skills/courseware/references/template-fidelity.md +100 -0
- package/skills/courseware/workflows/generate.md +172 -0
- package/skills/courseware/workflows/quick.md +41 -0
- package/skills/courseware/workflows/revise.md +40 -0
- package/skills/courseware/workflows/routing.md +49 -0
- package/skills/courseware/workflows/stages/generate-images.md +74 -0
- package/skills/courseware/workflows/template-intake.md +112 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""形状绘制层:文本框、自动缩放、项目符号、图片、图形。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Optional, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
from pptx.dml.color import RGBColor
|
|
11
|
+
from pptx.enum.shapes import MSO_SHAPE
|
|
12
|
+
from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN
|
|
13
|
+
from pptx.oxml.ns import qn
|
|
14
|
+
from pptx.util import Emu, Pt
|
|
15
|
+
|
|
16
|
+
from .util import (EMU_PER_INCH, EMU_PER_PT, emu_from_inch, parse_color,
|
|
17
|
+
pt_from_emu, rgb_tuple, wrap_lines)
|
|
18
|
+
|
|
19
|
+
ALIGN_MAP = {
|
|
20
|
+
"left": PP_ALIGN.LEFT, "l": PP_ALIGN.LEFT,
|
|
21
|
+
"center": PP_ALIGN.CENTER, "c": PP_ALIGN.CENTER, "centre": PP_ALIGN.CENTER,
|
|
22
|
+
"right": PP_ALIGN.RIGHT, "r": PP_ALIGN.RIGHT,
|
|
23
|
+
"justify": PP_ALIGN.JUSTIFY,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ANCHOR_MAP = {
|
|
27
|
+
"top": MSO_ANCHOR.TOP, "t": MSO_ANCHOR.TOP,
|
|
28
|
+
"middle": MSO_ANCHOR.MIDDLE, "center": MSO_ANCHOR.MIDDLE, "c": MSO_ANCHOR.MIDDLE,
|
|
29
|
+
"bottom": MSO_ANCHOR.BOTTOM, "b": MSO_ANCHOR.BOTTOM,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Para:
|
|
35
|
+
"""一段文字(一个段落)。"""
|
|
36
|
+
|
|
37
|
+
text: str = ""
|
|
38
|
+
size: float = 18.0
|
|
39
|
+
bold: bool = False
|
|
40
|
+
italic: bool = False
|
|
41
|
+
color: Optional[str] = None
|
|
42
|
+
font: Optional[str] = None
|
|
43
|
+
align: Optional[str] = None # None = 继承模板/默认左对齐
|
|
44
|
+
level: int = 0
|
|
45
|
+
bullet: Optional[str] = None # None 表示继承;"" 表示无
|
|
46
|
+
indent_in: float = 0.0
|
|
47
|
+
space_before: float = 0.0 # 磅
|
|
48
|
+
space_after: float = 0.0
|
|
49
|
+
line_spacing: float = 1.18 # 倍数
|
|
50
|
+
runs: Optional[List[dict]] = None # 可选:段内多 run(富文本)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def measure_block(paras: Sequence[Para], width_pt: float) -> float:
|
|
54
|
+
"""估算一段内容渲染后需要的高度(磅)。"""
|
|
55
|
+
total = 0.0
|
|
56
|
+
for p in paras:
|
|
57
|
+
size = max(p.size, 1.0)
|
|
58
|
+
text = p.text or ""
|
|
59
|
+
if p.runs:
|
|
60
|
+
text = "".join(r.get("text", "") for r in p.runs)
|
|
61
|
+
n_lines = max(1, wrap_lines(text, size, max(width_pt - p.indent_in * 72.0, 12.0), p.bold))
|
|
62
|
+
total += p.space_before + p.space_after
|
|
63
|
+
total += n_lines * size * 1.2 * p.line_spacing
|
|
64
|
+
return total
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fit_paras(paras: List[Para], width_pt: float, height_pt: float,
|
|
68
|
+
floor: float = 0.55, gap_rhythm: bool = True) -> List[Para]:
|
|
69
|
+
"""等比缩小字号,直到内容能塞进给定高度(有界迭代,不递归)。"""
|
|
70
|
+
if not paras or height_pt <= 1 or width_pt <= 1:
|
|
71
|
+
return paras
|
|
72
|
+
need = measure_block(paras, width_pt)
|
|
73
|
+
if need <= height_pt:
|
|
74
|
+
return paras
|
|
75
|
+
|
|
76
|
+
def build(k: float) -> List[Para]:
|
|
77
|
+
out = []
|
|
78
|
+
for p in paras:
|
|
79
|
+
q = Para(**{**p.__dict__})
|
|
80
|
+
q.size = max(9.0, p.size * k)
|
|
81
|
+
if gap_rhythm:
|
|
82
|
+
q.space_before = p.space_before * k
|
|
83
|
+
q.space_after = p.space_after * k
|
|
84
|
+
out.append(q)
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
k = max(floor, (height_pt / need) * 0.98)
|
|
88
|
+
cur = build(k)
|
|
89
|
+
for _ in range(14):
|
|
90
|
+
if measure_block(cur, width_pt) <= height_pt or k <= floor:
|
|
91
|
+
return cur
|
|
92
|
+
k = max(floor, k * 0.96)
|
|
93
|
+
cur = build(k)
|
|
94
|
+
return cur
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------
|
|
98
|
+
# 文本框
|
|
99
|
+
# --------------------------------------------------------------------------
|
|
100
|
+
def add_textbox(slide, box: Tuple[int, int, int, int], *, wrap: bool = True,
|
|
101
|
+
anchor: str = "top", margins=None):
|
|
102
|
+
x, y, cx, cy = box
|
|
103
|
+
tb = slide.shapes.add_textbox(Emu(int(x)), Emu(int(y)), Emu(int(max(cx, 1))), Emu(int(max(cy, 1))))
|
|
104
|
+
tf = tb.text_frame
|
|
105
|
+
tf.word_wrap = wrap
|
|
106
|
+
tf.auto_size = MSO_AUTO_SIZE.NONE
|
|
107
|
+
tf.vertical_anchor = ANCHOR_MAP.get(anchor, MSO_ANCHOR.TOP)
|
|
108
|
+
m = margins or (0.0, 0.0, 0.0, 0.0)
|
|
109
|
+
tf.margin_left = emu_from_inch(m[0])
|
|
110
|
+
tf.margin_right = emu_from_inch(m[1])
|
|
111
|
+
tf.margin_top = emu_from_inch(m[2])
|
|
112
|
+
tf.margin_bottom = emu_from_inch(m[3])
|
|
113
|
+
tb.name = "__pptxgen_text"
|
|
114
|
+
return tb
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def set_ea_font(run, font_name: str) -> None:
|
|
118
|
+
"""同时设置 latin / ea / cs 字体,保证中英文都用同一款字。"""
|
|
119
|
+
if not font_name:
|
|
120
|
+
return
|
|
121
|
+
run.font.name = font_name
|
|
122
|
+
rPr = run._r.get_or_add_rPr()
|
|
123
|
+
for tag in ("a:latin", "a:ea", "a:cs"):
|
|
124
|
+
el = rPr.find(qn(tag))
|
|
125
|
+
if el is None:
|
|
126
|
+
el = rPr.makeelement(qn(tag), {})
|
|
127
|
+
rPr.append(el)
|
|
128
|
+
el.set("typeface", font_name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def write_paras(tf, paras: Sequence[Para], *, default_font: Optional[str] = None) -> None:
|
|
132
|
+
"""把 Para 列表写入 text_frame(会清空原有内容)。"""
|
|
133
|
+
tf.clear()
|
|
134
|
+
if not paras:
|
|
135
|
+
return
|
|
136
|
+
for i, p in enumerate(paras):
|
|
137
|
+
para = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
138
|
+
_fill_paragraph(para, p, default_font)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _fill_paragraph(para, p: Para, default_font: Optional[str]) -> None:
|
|
142
|
+
if p.align:
|
|
143
|
+
para.alignment = ALIGN_MAP.get(str(p.align).lower(), PP_ALIGN.LEFT)
|
|
144
|
+
p.space_before = max(0.0, p.space_before)
|
|
145
|
+
pPr = para._p.get_or_add_pPr()
|
|
146
|
+
if p.space_before:
|
|
147
|
+
_set_spacing(pPr, "a:spcBef", int(p.space_before * 100))
|
|
148
|
+
if p.space_after:
|
|
149
|
+
_set_spacing(pPr, "a:spcAft", int(p.space_after * 100))
|
|
150
|
+
if p.line_spacing:
|
|
151
|
+
lnSpc = pPr.makeelement(qn("a:lnSpc"), {})
|
|
152
|
+
pct = lnSpc.makeelement(qn("a:spcPct"), {"val": str(int(p.line_spacing * 100000))})
|
|
153
|
+
lnSpc.append(pct)
|
|
154
|
+
pPr.insert(0, lnSpc)
|
|
155
|
+
|
|
156
|
+
if p.level:
|
|
157
|
+
pPr.set("lvl", str(min(int(p.level), 8)))
|
|
158
|
+
if p.indent_in:
|
|
159
|
+
pPr.set("marL", str(int(p.indent_in * EMU_PER_INCH)))
|
|
160
|
+
pPr.set("indent", str(-int(min(p.indent_in, 0.3) * EMU_PER_INCH)))
|
|
161
|
+
|
|
162
|
+
font_name = p.font or default_font
|
|
163
|
+
|
|
164
|
+
if p.runs:
|
|
165
|
+
_apply_bullet(pPr, p)
|
|
166
|
+
for r in p.runs:
|
|
167
|
+
run = para.add_run()
|
|
168
|
+
run.text = r.get("text", "")
|
|
169
|
+
_style_run(run, size=r.get("size", p.size), bold=r.get("bold", p.bold),
|
|
170
|
+
italic=r.get("italic", p.italic), color=r.get("color", p.color),
|
|
171
|
+
font=r.get("font", font_name))
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
_apply_bullet(pPr, p)
|
|
175
|
+
run = para.add_run()
|
|
176
|
+
run.text = p.text
|
|
177
|
+
_style_run(run, size=p.size, bold=p.bold, italic=p.italic, color=p.color, font=font_name)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _style_run(run, *, size, bold, italic, color, font) -> None:
|
|
181
|
+
run.font.size = Pt(max(1.0, float(size)))
|
|
182
|
+
run.font.bold = bool(bold)
|
|
183
|
+
run.font.italic = bool(italic)
|
|
184
|
+
if color:
|
|
185
|
+
hexv = parse_color(color)
|
|
186
|
+
if hexv:
|
|
187
|
+
run.font.color.rgb = RGBColor(*rgb_tuple(hexv))
|
|
188
|
+
if font:
|
|
189
|
+
set_ea_font(run, font)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _set_spacing(pPr, tag: str, points_x100: int) -> None:
|
|
193
|
+
"""写入 <a:spcBef>/<a:spcAft> 元素,其子元素必须是 <a:spcPts val="..."/>(OOXML 规范)。"""
|
|
194
|
+
el = pPr.find(qn(tag))
|
|
195
|
+
if el is None:
|
|
196
|
+
el = pPr.makeelement(qn(tag), {})
|
|
197
|
+
pPr.append(el)
|
|
198
|
+
for child in list(el):
|
|
199
|
+
el.remove(child)
|
|
200
|
+
child = el.makeelement(qn("a:spcPts"), {"val": str(int(points_x100))})
|
|
201
|
+
el.append(child)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _apply_bullet(pPr, p: Para) -> None:
|
|
205
|
+
if p.bullet is None:
|
|
206
|
+
return # 不干预,继承模板版式里的项目符号设置
|
|
207
|
+
existing = pPr.findall(qn("a:buNone")) + pPr.findall(qn("a:buChar")) + pPr.findall(qn("a:buAutoNum"))
|
|
208
|
+
for el in existing:
|
|
209
|
+
pPr.remove(el)
|
|
210
|
+
if p.bullet == "":
|
|
211
|
+
pPr.append(pPr.makeelement(qn("a:buNone"), {}))
|
|
212
|
+
return
|
|
213
|
+
if p.bullet == "num":
|
|
214
|
+
bu = pPr.makeelement(qn("a:buAutoNum"), {"type": "arabicPeriod"})
|
|
215
|
+
pPr.append(bu)
|
|
216
|
+
return
|
|
217
|
+
if p.color:
|
|
218
|
+
hexv = parse_color(p.color)
|
|
219
|
+
if hexv:
|
|
220
|
+
buClr = pPr.makeelement(qn("a:buClr"), {})
|
|
221
|
+
srgb = buClr.makeelement(qn("a:srgbClr"), {"val": hexv})
|
|
222
|
+
buClr.append(srgb)
|
|
223
|
+
pPr.append(buClr)
|
|
224
|
+
buFont = pPr.makeelement(qn("a:buFont"), {"typeface": "Arial"})
|
|
225
|
+
pPr.append(buFont)
|
|
226
|
+
buChar = pPr.makeelement(qn("a:buChar"), {"char": p.bullet})
|
|
227
|
+
pPr.append(buChar)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# --------------------------------------------------------------------------
|
|
231
|
+
# 图形
|
|
232
|
+
# --------------------------------------------------------------------------
|
|
233
|
+
def add_shape(slide, shape_type, box, *, fill=None, line=None, line_width_pt=1.0,
|
|
234
|
+
shadow: bool = False, fill_alpha: float = 1.0):
|
|
235
|
+
x, y, cx, cy = box
|
|
236
|
+
shp = slide.shapes.add_shape(shape_type, Emu(int(x)), Emu(int(y)),
|
|
237
|
+
Emu(int(max(cx, 1))), Emu(int(max(cy, 1))))
|
|
238
|
+
shp.name = "__pptxgen_shape"
|
|
239
|
+
if fill:
|
|
240
|
+
hexv = parse_color(fill)
|
|
241
|
+
shp.fill.solid()
|
|
242
|
+
shp.fill.fore_color.rgb = RGBColor(*rgb_tuple(hexv))
|
|
243
|
+
if fill_alpha < 1.0:
|
|
244
|
+
_set_fill_alpha(shp, fill_alpha)
|
|
245
|
+
else:
|
|
246
|
+
shp.fill.background()
|
|
247
|
+
if line:
|
|
248
|
+
hexv = parse_color(line)
|
|
249
|
+
shp.line.color.rgb = RGBColor(*rgb_tuple(hexv))
|
|
250
|
+
shp.line.width = Pt(line_width_pt)
|
|
251
|
+
else:
|
|
252
|
+
shp.line.fill.background()
|
|
253
|
+
if not shadow:
|
|
254
|
+
_no_shadow(shp)
|
|
255
|
+
shp.text_frame.word_wrap = True
|
|
256
|
+
return shp
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _no_shadow(shp) -> None:
|
|
260
|
+
spPr = shp._element.spPr
|
|
261
|
+
existing = spPr.find(qn("a:effectLst"))
|
|
262
|
+
if existing is not None:
|
|
263
|
+
spPr.remove(existing)
|
|
264
|
+
spPr.append(spPr.makeelement(qn("a:effectLst"), {}))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _set_fill_alpha(shp, alpha: float) -> None:
|
|
268
|
+
solid = shp._element.spPr.find(qn("a:solidFill"))
|
|
269
|
+
if solid is None:
|
|
270
|
+
return
|
|
271
|
+
srgb = solid.find(qn("a:srgbClr"))
|
|
272
|
+
if srgb is None:
|
|
273
|
+
return
|
|
274
|
+
for old in srgb.findall(qn("a:alpha")):
|
|
275
|
+
srgb.remove(old)
|
|
276
|
+
srgb.append(srgb.makeelement(qn("a:alpha"), {"val": str(int(max(0.0, min(1.0, alpha)) * 100000))}))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def set_shape_transparency(shp, alpha: float) -> None:
|
|
280
|
+
_set_fill_alpha(shp, alpha)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def rounded_box(slide, box, *, fill=None, line=None, radius: float = 0.12, **kw):
|
|
284
|
+
shp = add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, box, fill=fill, line=line, **kw)
|
|
285
|
+
try:
|
|
286
|
+
shp.adjustments[0] = max(0.0, min(0.5, radius))
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
return shp
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def add_line(slide, x1, y1, x2, y2, *, color=None, width_pt=1.5, dash=None):
|
|
293
|
+
from pptx.enum.shapes import MSO_CONNECTOR
|
|
294
|
+
|
|
295
|
+
conn = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, Emu(int(x1)), Emu(int(y1)),
|
|
296
|
+
Emu(int(x2)), Emu(int(y2)))
|
|
297
|
+
conn.name = "__pptxgen_line"
|
|
298
|
+
if color:
|
|
299
|
+
conn.line.color.rgb = RGBColor(*rgb_tuple(parse_color(color)))
|
|
300
|
+
conn.line.width = Pt(width_pt)
|
|
301
|
+
if dash:
|
|
302
|
+
conn.line.dash_style = dash
|
|
303
|
+
return conn
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# --------------------------------------------------------------------------
|
|
307
|
+
# 图片
|
|
308
|
+
# --------------------------------------------------------------------------
|
|
309
|
+
def image_size(path: str) -> Tuple[int, int]:
|
|
310
|
+
try:
|
|
311
|
+
from PIL import Image
|
|
312
|
+
|
|
313
|
+
with Image.open(path) as im:
|
|
314
|
+
return im.size
|
|
315
|
+
except Exception:
|
|
316
|
+
return (4, 3)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def add_picture_fit(slide, path: str, box, *, mode: str = "contain", align: str = "center",
|
|
320
|
+
valign: str = "middle", border=None, radius=None):
|
|
321
|
+
"""把图片按比例放进 box。mode: contain | cover | stretch。"""
|
|
322
|
+
x, y, cx, cy = box
|
|
323
|
+
iw, ih = image_size(path)
|
|
324
|
+
if iw <= 0 or ih <= 0:
|
|
325
|
+
iw, ih = 4, 3
|
|
326
|
+
box_ar = cx / float(cy) if cy else 1.0
|
|
327
|
+
img_ar = iw / float(ih)
|
|
328
|
+
|
|
329
|
+
if mode == "stretch":
|
|
330
|
+
return slide.shapes.add_picture(path, Emu(int(x)), Emu(int(y)), Emu(int(cx)), Emu(int(cy)))
|
|
331
|
+
|
|
332
|
+
if mode == "cover":
|
|
333
|
+
if img_ar > box_ar:
|
|
334
|
+
h = cy
|
|
335
|
+
w = int(cy * img_ar)
|
|
336
|
+
else:
|
|
337
|
+
w = cx
|
|
338
|
+
h = int(cx / img_ar)
|
|
339
|
+
left = int(x + (cx - w) * {"left": 0, "center": 0.5, "right": 1}.get(align, 0.5))
|
|
340
|
+
top = int(y + (cy - h) * {"top": 0, "middle": 0.5, "bottom": 1}.get(valign, 0.5))
|
|
341
|
+
pic = slide.shapes.add_picture(path, Emu(left), Emu(top), Emu(int(w)), Emu(int(h)))
|
|
342
|
+
else: # contain
|
|
343
|
+
if img_ar > box_ar:
|
|
344
|
+
w = cx
|
|
345
|
+
h = int(cx / img_ar)
|
|
346
|
+
else:
|
|
347
|
+
h = cy
|
|
348
|
+
w = int(cy * img_ar)
|
|
349
|
+
left = int(x + (cx - w) * {"left": 0, "center": 0.5, "right": 1}.get(align, 0.5))
|
|
350
|
+
top = int(y + (cy - h) * {"top": 0, "middle": 0.5, "bottom": 1}.get(valign, 0.5))
|
|
351
|
+
pic = slide.shapes.add_picture(path, Emu(left), Emu(top), Emu(int(w)), Emu(int(h)))
|
|
352
|
+
|
|
353
|
+
pic.name = "__pptxgen_pic"
|
|
354
|
+
if border:
|
|
355
|
+
pic.line.color.rgb = RGBColor(*rgb_tuple(parse_color(border)))
|
|
356
|
+
pic.line.width = Pt(1.0)
|
|
357
|
+
if radius is not None:
|
|
358
|
+
_round_picture(pic, radius)
|
|
359
|
+
return pic
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _round_picture(pic, radius: float) -> None:
|
|
363
|
+
"""给图片加圆角(通过 prstGeom roundRect + 裁剪)。"""
|
|
364
|
+
spPr = pic._element.spPr
|
|
365
|
+
old = spPr.find(qn("a:prstGeom"))
|
|
366
|
+
if old is not None:
|
|
367
|
+
spPr.remove(old)
|
|
368
|
+
geom = spPr.makeelement(qn("a:prstGeom"), {"prst": "roundRect"})
|
|
369
|
+
avLst = geom.makeelement(qn("a:avLst"), {})
|
|
370
|
+
gd = avLst.makeelement(qn("a:gd"), {"name": "adj", "fmla": f"val {int(radius * 100000)}"})
|
|
371
|
+
avLst.append(gd)
|
|
372
|
+
geom.append(avLst)
|
|
373
|
+
xfrm = spPr.find(qn("a:xfrm"))
|
|
374
|
+
if xfrm is not None:
|
|
375
|
+
xfrm.addnext(geom)
|
|
376
|
+
else:
|
|
377
|
+
spPr.append(geom)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def add_rect_picture_clip(slide, path: str, box, radius: float = 0.1):
|
|
381
|
+
"""图片填充的圆角矩形(模板里常见的卡片式配图)。"""
|
|
382
|
+
x, y, cx, cy = box
|
|
383
|
+
shp = add_shape(slide, MSO_SHAPE.ROUNDED_RECTANGLE, box, fill="FFFFFF", line=None)
|
|
384
|
+
try:
|
|
385
|
+
shp.adjustments[0] = radius
|
|
386
|
+
except Exception:
|
|
387
|
+
pass
|
|
388
|
+
fill = shp.fill
|
|
389
|
+
fill.solid()
|
|
390
|
+
fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
|
|
391
|
+
_set_blip_fill(shp, path)
|
|
392
|
+
return shp
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _set_blip_fill(shp, path: str) -> None:
|
|
396
|
+
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
|
|
397
|
+
|
|
398
|
+
part = shp.part
|
|
399
|
+
with open(path, "rb") as f:
|
|
400
|
+
blob = f.read()
|
|
401
|
+
image_part, rId = part.get_or_add_image_part(io.BytesIO(blob))
|
|
402
|
+
spPr = shp._element.spPr
|
|
403
|
+
for tag in ("a:solidFill", "a:noFill", "a:gradFill", "a:blipFill"):
|
|
404
|
+
el = spPr.find(qn(tag))
|
|
405
|
+
if el is not None:
|
|
406
|
+
spPr.remove(el)
|
|
407
|
+
blipFill = spPr.makeelement(qn("a:blipFill"), {})
|
|
408
|
+
blip = blipFill.makeelement(qn("a:blip"), {qn("r:embed"): rId})
|
|
409
|
+
blipFill.append(blip)
|
|
410
|
+
stretch = blipFill.makeelement(qn("a:stretch"), {})
|
|
411
|
+
stretch.append(stretch.makeelement(qn("a:fillRect"), {}))
|
|
412
|
+
blipFill.append(stretch)
|
|
413
|
+
ln = spPr.find(qn("a:ln"))
|
|
414
|
+
if ln is not None:
|
|
415
|
+
ln.addprevious(blipFill)
|
|
416
|
+
else:
|
|
417
|
+
spPr.append(blipFill)
|