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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/cordis.patch.yml +9 -0
  4. package/engine/pptxgen/__init__.py +8 -0
  5. package/engine/pptxgen/__main__.py +4 -0
  6. package/engine/pptxgen/builtin_template.py +218 -0
  7. package/engine/pptxgen/cli.py +575 -0
  8. package/engine/pptxgen/examples.py +290 -0
  9. package/engine/pptxgen/imagegen.py +564 -0
  10. package/engine/pptxgen/layoutmap.py +211 -0
  11. package/engine/pptxgen/patterns.py +106 -0
  12. package/engine/pptxgen/preview.py +126 -0
  13. package/engine/pptxgen/profile.py +456 -0
  14. package/engine/pptxgen/render.py +1673 -0
  15. package/engine/pptxgen/review.py +184 -0
  16. package/engine/pptxgen/schemacheck.py +122 -0
  17. package/engine/pptxgen/schemas/XAdES.xsd +466 -0
  18. package/engine/pptxgen/schemas/XAdESv141.xsd +15 -0
  19. package/engine/pptxgen/schemas/chartEx.xsd +838 -0
  20. package/engine/pptxgen/schemas/dml-chart.xsd +1499 -0
  21. package/engine/pptxgen/schemas/dml-chartDrawing.xsd +146 -0
  22. package/engine/pptxgen/schemas/dml-diagram.xsd +1085 -0
  23. package/engine/pptxgen/schemas/dml-drawing.xsd +63 -0
  24. package/engine/pptxgen/schemas/dml-lockedCanvas.xsd +11 -0
  25. package/engine/pptxgen/schemas/dml-main.xsd +3081 -0
  26. package/engine/pptxgen/schemas/dml-picture.xsd +23 -0
  27. package/engine/pptxgen/schemas/dml-spreadsheetDrawing.xsd +185 -0
  28. package/engine/pptxgen/schemas/dml-wordprocessingDrawing.xsd +287 -0
  29. package/engine/pptxgen/schemas/drawing-chart2012.xsd +129 -0
  30. package/engine/pptxgen/schemas/markup-compatibility.xsd +95 -0
  31. package/engine/pptxgen/schemas/opc-digSig.xsd +49 -0
  32. package/engine/pptxgen/schemas/opc-relationships.xsd +33 -0
  33. package/engine/pptxgen/schemas/pml.xsd +1676 -0
  34. package/engine/pptxgen/schemas/shared-additionalCharacteristics.xsd +28 -0
  35. package/engine/pptxgen/schemas/shared-bibliography.xsd +144 -0
  36. package/engine/pptxgen/schemas/shared-commonSimpleTypes.xsd +172 -0
  37. package/engine/pptxgen/schemas/shared-customXmlDataProperties.xsd +25 -0
  38. package/engine/pptxgen/schemas/shared-customXmlSchemaProperties.xsd +18 -0
  39. package/engine/pptxgen/schemas/shared-documentPropertiesCustom.xsd +59 -0
  40. package/engine/pptxgen/schemas/shared-documentPropertiesExtended.xsd +56 -0
  41. package/engine/pptxgen/schemas/shared-documentPropertiesVariantTypes.xsd +195 -0
  42. package/engine/pptxgen/schemas/shared-math.xsd +582 -0
  43. package/engine/pptxgen/schemas/shared-relationshipReference.xsd +25 -0
  44. package/engine/pptxgen/schemas/signatureInfo.xsd +103 -0
  45. package/engine/pptxgen/schemas/sml.xsd +4439 -0
  46. package/engine/pptxgen/schemas/visio.xsd +829 -0
  47. package/engine/pptxgen/schemas/vml-main.xsd +570 -0
  48. package/engine/pptxgen/schemas/vml-officeDrawing.xsd +509 -0
  49. package/engine/pptxgen/schemas/vml-presentationDrawing.xsd +12 -0
  50. package/engine/pptxgen/schemas/vml-spreadsheetDrawing.xsd +108 -0
  51. package/engine/pptxgen/schemas/vml-wordprocessingDrawing.xsd +96 -0
  52. package/engine/pptxgen/schemas/vmlDrawing.xsd +36 -0
  53. package/engine/pptxgen/schemas/wml.xsd +3643 -0
  54. package/engine/pptxgen/schemas/word12.xsd +66 -0
  55. package/engine/pptxgen/schemas/xlThreadedComments.xsd +59 -0
  56. package/engine/pptxgen/schemas/xlThreadedComments2.xsd +22 -0
  57. package/engine/pptxgen/schemas/xmldsig-core-schema.xsd +318 -0
  58. package/engine/pptxgen/shapes.py +417 -0
  59. package/engine/pptxgen/spec.py +529 -0
  60. package/engine/pptxgen/template.py +834 -0
  61. package/engine/pptxgen/util.py +218 -0
  62. package/engine/pptxgen/xmlutil.py +298 -0
  63. package/engine/pptxgen.sh +7 -0
  64. package/engine/requirements.txt +9 -0
  65. package/lib/index.js +186 -0
  66. package/package.json +34 -0
  67. package/skills/courseware/SKILL.md +130 -0
  68. package/skills/courseware/references/content-polish.md +121 -0
  69. package/skills/courseware/references/deck-spec.md +155 -0
  70. package/skills/courseware/references/design-review.md +121 -0
  71. package/skills/courseware/references/image-backends.md +94 -0
  72. package/skills/courseware/references/quality-gates.md +71 -0
  73. package/skills/courseware/references/roadmap.md +55 -0
  74. package/skills/courseware/references/template-fidelity.md +100 -0
  75. package/skills/courseware/workflows/generate.md +172 -0
  76. package/skills/courseware/workflows/quick.md +41 -0
  77. package/skills/courseware/workflows/revise.md +40 -0
  78. package/skills/courseware/workflows/routing.md +49 -0
  79. package/skills/courseware/workflows/stages/generate-images.md +74 -0
  80. 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)