python-pptx2 2.13.0__py3-none-any.whl

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 (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,597 @@
1
+ """Slide import machinery for Presentation.import_slide().
2
+
3
+ Copies a slide from one |Presentation| into another, rewriting part-names
4
+ and relationship targets so the imported slide is indistinguishable from a
5
+ natively authored one.
6
+
7
+ The public entry point is :func:`import_slide`. Everything else is an
8
+ implementation detail.
9
+
10
+ Supported ``merge_master`` values:
11
+
12
+ ``'dedupe'``
13
+ Reuse an existing destination master when its (normalised) XML fingerprint
14
+ matches the source master. Otherwise clone the master.
15
+
16
+ ``'clone'``
17
+ Always clone the master and its layout/theme parts, even when the
18
+ destination already has an identical master.
19
+
20
+ Copied content
21
+ --------------
22
+ The following slide-level parts are copied:
23
+
24
+ * The slide itself.
25
+ * Its notes slide (if any).
26
+ * All image / media / chart / OLE-object / SmartArt-diagram / video parts
27
+ reachable from the slide.
28
+ * The slide layout (always cloned — layouts belong to a specific master).
29
+ * The slide master and its theme (deduped or cloned, see above).
30
+
31
+ The following are intentionally **not** deep-copied:
32
+
33
+ * The notes master — the copied notes slide is re-linked to the destination's
34
+ own notes master; the source's is cloned only when the destination has none.
35
+ * The handout master.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import hashlib
41
+ from copy import deepcopy
42
+ from typing import TYPE_CHECKING, Literal
43
+
44
+ from pptx2.opc.constants import RELATIONSHIP_TYPE as RT
45
+ from pptx2.opc.package import Part, PartFactory, XmlPart
46
+ from pptx2.opc.packuri import PackURI
47
+
48
+ if TYPE_CHECKING:
49
+ from pptx2.opc.package import _Relationships # pyright: ignore[reportPrivateUsage]
50
+ from pptx2.package import Package
51
+ from pptx2.parts.presentation import PresentationPart
52
+ from pptx2.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
53
+ from pptx2.slide import Slide
54
+
55
+ MergeMaster = Literal["dedupe", "clone"]
56
+
57
+ # Relationship types whose target parts belong to the master / layout hierarchy
58
+ # and are therefore handled separately from the general part graph copy.
59
+ _MASTER_HIERARCHY_RELTYPES = frozenset(
60
+ {
61
+ RT.SLIDE_LAYOUT,
62
+ RT.SLIDE_MASTER,
63
+ RT.THEME,
64
+ }
65
+ )
66
+
67
+ # Relationship types for parts that travel with the slide (non-master deps)
68
+ _NOTES_MASTER_RELTYPE = RT.NOTES_MASTER
69
+
70
+
71
+ def import_slide(
72
+ src_slide_part: SlidePart,
73
+ dst_prs_part: PresentationPart,
74
+ merge_master: MergeMaster = "dedupe",
75
+ ) -> Slide:
76
+ """Copy *src_slide_part* into *dst_prs_part* and return the new |Slide|.
77
+
78
+ Parameters
79
+ ----------
80
+ src_slide_part:
81
+ The :class:`~pptx2.parts.slide.SlidePart` from the source presentation.
82
+ dst_prs_part:
83
+ The :class:`~pptx2.parts.presentation.PresentationPart` of the target.
84
+ merge_master:
85
+ ``'dedupe'`` (default) or ``'clone'`` — controls master handling.
86
+
87
+ Returns
88
+ -------
89
+ Slide
90
+ The newly imported :class:`~pptx2.slide.Slide` object.
91
+ """
92
+ importer = _SlideImporter(src_slide_part, dst_prs_part, merge_master)
93
+ return importer.run()
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Internal implementation
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ class _SlideImporter:
102
+ """Stateful helper that performs the import operation."""
103
+
104
+ def __init__(
105
+ self,
106
+ src_slide_part: SlidePart,
107
+ dst_prs_part: PresentationPart,
108
+ merge_master: MergeMaster,
109
+ ) -> None:
110
+ self._src_slide_part = src_slide_part
111
+ self._dst_prs_part = dst_prs_part
112
+ self._merge_master = merge_master
113
+ self._dst_package: Package = dst_prs_part.package # type: ignore[assignment]
114
+ # Mapping: source Part → copied destination Part
115
+ self._part_map: dict[Part, Part] = {}
116
+ # Partnames already picked in this import run (but not yet in the package graph)
117
+ self._reserved_partnames: set[str] = set()
118
+
119
+ def run(self) -> Slide:
120
+ """Execute the import and return the new Slide."""
121
+ dst_layout_part = self._resolve_layout()
122
+ dst_slide_part = self._copy_slide(dst_layout_part)
123
+ self._register_slide(dst_slide_part)
124
+ return dst_slide_part.slide
125
+
126
+ def _next_partname(self, tmpl: str) -> PackURI:
127
+ """Return the next non-colliding partname for *tmpl*, accounting for parts
128
+ not yet in the package graph that have already been reserved in this run."""
129
+ prefix = tmpl[: (tmpl % 42).find("42")]
130
+ existing = {
131
+ p.partname for p in self._dst_package.iter_parts()
132
+ if p.partname.startswith(prefix)
133
+ }
134
+ taken = existing | {pn for pn in self._reserved_partnames if pn.startswith(prefix)}
135
+ n = 1
136
+ while True:
137
+ candidate = tmpl % n
138
+ if candidate not in taken:
139
+ self._reserved_partnames.add(candidate)
140
+ return PackURI(candidate)
141
+ n += 1
142
+
143
+ # ------------------------------------------------------------------
144
+ # Master / layout resolution
145
+ # ------------------------------------------------------------------
146
+
147
+ def _resolve_layout(self) -> SlideLayoutPart:
148
+ """Return the destination layout part for the imported slide.
149
+
150
+ Either reuses an existing destination master (dedupe) or clones a new
151
+ one (clone or dedupe miss).
152
+ """
153
+ src_layout_part: SlideLayoutPart = self._src_slide_part.part_related_by(RT.SLIDE_LAYOUT) # type: ignore[assignment]
154
+ src_master_part: SlideMasterPart = src_layout_part.part_related_by(RT.SLIDE_MASTER) # type: ignore[assignment]
155
+
156
+ if self._merge_master == "dedupe":
157
+ existing = self._find_matching_master(src_master_part)
158
+ if existing is not None:
159
+ return self._find_or_clone_layout_in_master(src_layout_part, existing)
160
+
161
+ # Clone master + theme + all layouts
162
+ return self._clone_master_with_layout(src_master_part, src_layout_part)
163
+
164
+ def _find_matching_master(self, src_master_part: SlideMasterPart) -> SlideMasterPart | None:
165
+ """Return a destination master part whose fingerprint matches src_master_part, or None."""
166
+ src_fp = _master_fingerprint(src_master_part)
167
+ for dst_master_part in self._iter_dst_masters():
168
+ if _master_fingerprint(dst_master_part) == src_fp:
169
+ return dst_master_part
170
+ return None
171
+
172
+ def _iter_dst_masters(self): # type: ignore[return]
173
+ """Yield each SlideMasterPart already in the destination presentation."""
174
+ prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
175
+ if prs_element.sldMasterIdLst is None:
176
+ return
177
+ for entry in prs_element.sldMasterIdLst.sldMasterId_lst:
178
+ yield self._dst_prs_part.related_part(entry.rId)
179
+
180
+ def _find_or_clone_layout_in_master(
181
+ self, src_layout_part: SlideLayoutPart, dst_master_part: SlideMasterPart
182
+ ) -> SlideLayoutPart:
183
+ """Return the layout in *dst_master_part* that best matches *src_layout_part*.
184
+
185
+ Matching order:
186
+ 1. Same ``<p:cSld name="…">``
187
+ 2. Same layout ``type`` attribute
188
+ 3. Fall back to first layout in master
189
+
190
+ If no layout matches either criterion, the source layout is cloned
191
+ into the destination master.
192
+ """
193
+ src_name = src_layout_part._element.cSld.name # pyright: ignore[reportPrivateUsage]
194
+ src_type = src_layout_part._element.get("type") # pyright: ignore[reportPrivateUsage]
195
+
196
+ # Walk existing layouts on the destination master
197
+ from pptx2.opc.constants import RELATIONSHIP_TYPE as RT2
198
+
199
+ layout_candidates: list[SlideLayoutPart] = []
200
+ for rel in dst_master_part.rels.values():
201
+ if rel.is_external or rel.reltype != RT2.SLIDE_LAYOUT:
202
+ continue
203
+ layout_candidates.append(rel.target_part) # type: ignore[arg-type]
204
+
205
+ # Priority 1: name match
206
+ for lp in layout_candidates:
207
+ if lp._element.cSld.name == src_name: # pyright: ignore[reportPrivateUsage]
208
+ return lp # type: ignore[return-value]
209
+
210
+ # Priority 2: type match
211
+ if src_type:
212
+ for lp in layout_candidates:
213
+ if lp._element.get("type") == src_type: # pyright: ignore[reportPrivateUsage]
214
+ return lp # type: ignore[return-value]
215
+
216
+ # Priority 3: clone the layout into the existing master
217
+ return self._clone_layout_into_master(src_layout_part, dst_master_part)
218
+
219
+ def _clone_layout_into_master(
220
+ self, src_layout_part: SlideLayoutPart, dst_master_part: SlideMasterPart
221
+ ) -> SlideLayoutPart:
222
+ """Clone *src_layout_part* and attach it to *dst_master_part*.
223
+
224
+ Copies the layout's own dependencies (background pictures etc.),
225
+ wires the layout ↔ master relationships, and registers the layout in
226
+ the master's `p:sldLayoutIdLst` with a fresh unique id — an
227
+ unregistered layout is invisible to PowerPoint's layout picker and
228
+ leaves the master's id list inconsistent with its relationships.
229
+ """
230
+ new_partname = self._next_partname("/ppt/slideLayouts/slideLayout%d.xml")
231
+ dst_layout_part = _clone_xml_part(src_layout_part, new_partname, self._dst_package)
232
+ id_map = self._copy_dependencies(src_layout_part, dst_layout_part)
233
+ _remap_rids(dst_layout_part._element, id_map) # pyright: ignore[reportPrivateUsage]
234
+ # Relate layout → master
235
+ dst_layout_part.relate_to(dst_master_part, RT.SLIDE_MASTER)
236
+ # Relate master → layout
237
+ rId = dst_master_part.relate_to(dst_layout_part, RT.SLIDE_LAYOUT)
238
+ # Register in the master's layout-id list
239
+ master_element = dst_master_part._element # pyright: ignore[reportPrivateUsage]
240
+ sldLayoutIdLst = master_element.get_or_add_sldLayoutIdLst()
241
+ entry = sldLayoutIdLst._add_sldLayoutId(rId=rId)
242
+ entry.set("id", str(self._next_hierarchy_id()))
243
+ return dst_layout_part # type: ignore[return-value]
244
+
245
+ def _clone_master_with_layout(
246
+ self, src_master_part: SlideMasterPart, src_layout_part: SlideLayoutPart
247
+ ) -> SlideLayoutPart:
248
+ """Clone the master (+ theme + all of its layouts), register with the destination.
249
+
250
+ Returns the cloned layout corresponding to *src_layout_part*.
251
+ """
252
+ dst_package = self._dst_package
253
+
254
+ # --- Clone theme ---
255
+ dst_theme_part: Part | None = None
256
+ try:
257
+ src_theme_part = src_master_part.part_related_by(RT.THEME)
258
+ theme_partname = self._next_partname("/ppt/theme/theme%d.xml")
259
+ dst_theme_part = _clone_part(src_theme_part, theme_partname, dst_package)
260
+ except KeyError:
261
+ pass # some masters have no theme
262
+
263
+ # --- Clone master ---
264
+ master_partname = self._next_partname("/ppt/slideMasters/slideMaster%d.xml")
265
+ dst_master_part = _clone_xml_part(src_master_part, master_partname, dst_package)
266
+ if dst_theme_part is not None:
267
+ dst_master_part.relate_to(dst_theme_part, RT.THEME)
268
+ id_map = self._copy_dependencies(src_master_part, dst_master_part)
269
+ _remap_rids(dst_master_part._element, id_map) # pyright: ignore[reportPrivateUsage]
270
+
271
+ # The deep-copied master XML still carries the *source* master's
272
+ # `p:sldLayoutIdLst` — its r:id values point at whatever landed on
273
+ # those rIds in the new rels (the theme, off-by-one layouts), which
274
+ # PowerPoint cannot resolve. Empty it here and rebuild it entry by
275
+ # entry as each layout is cloned below.
276
+ master_element = dst_master_part._element # pyright: ignore[reportPrivateUsage]
277
+ sldLayoutIdLst = master_element.get_or_add_sldLayoutIdLst()
278
+ for stale_entry in list(sldLayoutIdLst):
279
+ sldLayoutIdLst.remove(stale_entry)
280
+
281
+ # --- Register new master with presentation (fresh unique id) ---
282
+ rId = self._dst_prs_part.relate_to(dst_master_part, RT.SLIDE_MASTER)
283
+ prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
284
+ sldMasterIdLst = prs_element.get_or_add_sldMasterIdLst()
285
+ master_entry = sldMasterIdLst._add_sldMasterId(rId=rId) # pyright: ignore[reportAttributeAccessIssue]
286
+ master_entry.set("id", str(self._next_hierarchy_id()))
287
+
288
+ # --- Clone layouts, preserving the source master's layout order ---
289
+ src_to_dst_layout: dict[SlideLayoutPart, SlideLayoutPart] = {}
290
+ for src_lo in self._iter_master_layouts(src_master_part):
291
+ src_to_dst_layout[src_lo] = self._clone_layout_into_master(src_lo, dst_master_part)
292
+
293
+ # Return the cloned version of the source layout
294
+ dst_layout = src_to_dst_layout.get(src_layout_part)
295
+ if dst_layout is None:
296
+ # Fallback: use the first available layout
297
+ dst_layout = next(iter(src_to_dst_layout.values()), None)
298
+ if dst_layout is None:
299
+ # Emergency: clone the layout directly
300
+ dst_layout = self._clone_layout_into_master(src_layout_part, dst_master_part) # type: ignore[assignment]
301
+ return dst_layout # type: ignore[return-value]
302
+
303
+ def _iter_master_layouts(self, master_part: SlideMasterPart) -> list[SlideLayoutPart]:
304
+ """Return *master_part*'s layout parts, in `p:sldLayoutIdLst` order.
305
+
306
+ Falls back to relationship order for a master whose layout-id list is
307
+ absent or entirely unresolvable.
308
+ """
309
+ layouts: list[SlideLayoutPart] = []
310
+ sldLayoutIdLst = master_part._element.sldLayoutIdLst # pyright: ignore[reportPrivateUsage]
311
+ if sldLayoutIdLst is not None:
312
+ for entry in sldLayoutIdLst.sldLayoutId_lst:
313
+ try:
314
+ layouts.append(master_part.related_part(entry.rId)) # type: ignore[arg-type]
315
+ except KeyError:
316
+ continue
317
+ if layouts:
318
+ return layouts
319
+ return [
320
+ rel.target_part # type: ignore[misc]
321
+ for rel in master_part.rels.values()
322
+ if not rel.is_external and rel.reltype == RT.SLIDE_LAYOUT
323
+ ]
324
+
325
+ def _next_hierarchy_id(self) -> int:
326
+ """Return a fresh unique id for a `p:sldMasterId` / `p:sldLayoutId` entry.
327
+
328
+ These share one id space starting at 2147483648 (ST_SlideMasterId /
329
+ ST_SlideLayoutId minimum); duplicates across the presentation are a
330
+ repair trigger, so scan every master's layout-id list plus the
331
+ presentation's master-id list for the current maximum.
332
+ """
333
+ used = [2147483647]
334
+ prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
335
+ used += [int(v) for v in prs_element.xpath("p:sldMasterIdLst/p:sldMasterId/@id")]
336
+ for dst_master_part in self._iter_dst_masters():
337
+ used += [
338
+ int(v)
339
+ for v in dst_master_part._element.xpath( # pyright: ignore[reportPrivateUsage]
340
+ "p:sldLayoutIdLst/p:sldLayoutId/@id"
341
+ )
342
+ ]
343
+ return max(used) + 1
344
+
345
+ # ------------------------------------------------------------------
346
+ # Slide copy
347
+ # ------------------------------------------------------------------
348
+
349
+ def _copy_slide(self, dst_layout_part: SlideLayoutPart) -> SlidePart:
350
+ """Return a new SlidePart copied from the source slide.
351
+
352
+ All non-master-hierarchy dependencies (images, charts, media, notes, etc.)
353
+ are also copied. The new slide is related to *dst_layout_part*.
354
+ """
355
+ dst_package = self._dst_package
356
+ new_partname = self._next_partname("/ppt/slides/slide%d.xml")
357
+ dst_slide_part = _clone_xml_part(self._src_slide_part, new_partname, dst_package)
358
+
359
+ # Seed the part map with the slide itself so a copied notes slide's
360
+ # back-reference to its slide resolves to *this* part rather than
361
+ # triggering a second, orphan clone of the whole slide graph.
362
+ self._part_map[self._src_slide_part] = dst_slide_part
363
+
364
+ # Copy all deps except master-hierarchy rels. Track source-rId ->
365
+ # destination-rId so the cloned slide XML's embedded references
366
+ # (r:embed, r:id, …) can be rewritten to the new ids.
367
+ id_map = self._copy_dependencies(self._src_slide_part, dst_slide_part)
368
+
369
+ # Always wire layout relationship; map the source layout rId onto the new
370
+ # one in case the slide XML references it.
371
+ layout_rId = dst_slide_part.relate_to(dst_layout_part, RT.SLIDE_LAYOUT)
372
+ for rel in self._src_slide_part.rels.values():
373
+ if rel.reltype == RT.SLIDE_LAYOUT:
374
+ id_map[rel.rId] = layout_rId
375
+ break
376
+
377
+ _remap_rids(dst_slide_part._element, id_map) # pyright: ignore[reportPrivateUsage]
378
+ return dst_slide_part # type: ignore[return-value]
379
+
380
+ def _copy_dependencies(self, src_part: Part, dst_part: Part) -> dict[str, str]:
381
+ """Copy *src_part*'s dependencies onto *dst_part*; return the rId map.
382
+
383
+ External relationships are re-created verbatim; master-hierarchy
384
+ relationships (layout / master / theme) are skipped — callers wire
385
+ those explicitly; a notes-master relationship is re-pointed at the
386
+ destination's own notes master (importing the source's only when the
387
+ destination has none). Every other related part is copied
388
+ recursively. The caller is responsible for running the returned
389
+ source-rId → destination-rId map through :func:`_remap_rids`.
390
+ """
391
+ id_map: dict[str, str] = {}
392
+ for rel in src_part.rels.values():
393
+ if rel.is_external:
394
+ id_map[rel.rId] = dst_part.relate_to(
395
+ rel.target_ref, rel.reltype, is_external=True
396
+ )
397
+ continue
398
+ if rel.reltype in _MASTER_HIERARCHY_RELTYPES:
399
+ continue
400
+ if rel.reltype == _NOTES_MASTER_RELTYPE:
401
+ # Re-point at the destination's own notes master (created
402
+ # from the default template when absent) — a notes slide
403
+ # without its notesSlide→notesMaster relationship risks the
404
+ # repair prompt when notes view is opened or notes printed.
405
+ dst_notes_master = self._dst_prs_part.notes_master_part
406
+ id_map[rel.rId] = dst_part.relate_to(dst_notes_master, RT.NOTES_MASTER)
407
+ continue
408
+ dst_dep = self._copy_part_recursive(rel.target_part)
409
+ id_map[rel.rId] = dst_part.relate_to(dst_dep, rel.reltype)
410
+ return id_map
411
+
412
+ def _copy_part_recursive(self, src_part: Part) -> Part:
413
+ """Return a copy of *src_part* in the destination package.
414
+
415
+ Recursively copies all related parts (depth-first). Each source part
416
+ is copied at most once; subsequent calls for the same part return the
417
+ already-copied destination counterpart.
418
+ """
419
+ if src_part in self._part_map:
420
+ return self._part_map[src_part]
421
+
422
+ dst_package = self._dst_package
423
+ new_partname = self._next_partname(_partname_template(src_part.partname))
424
+ dst_part = _clone_part(src_part, new_partname, dst_package)
425
+ self._part_map[src_part] = dst_part
426
+
427
+ id_map = self._copy_dependencies(src_part, dst_part)
428
+
429
+ # Rewrite embedded rId references in cloned XML parts (e.g. a chart's
430
+ # <c:externalData r:id=…> pointing at its embedded workbook).
431
+ if isinstance(dst_part, XmlPart):
432
+ _remap_rids(dst_part._element, id_map) # pyright: ignore[reportPrivateUsage]
433
+
434
+ return dst_part
435
+
436
+ # ------------------------------------------------------------------
437
+ # Presentation registration
438
+ # ------------------------------------------------------------------
439
+
440
+ def _register_slide(self, dst_slide_part: SlidePart) -> None:
441
+ """Add the new slide part to the destination presentation."""
442
+ rId = self._dst_prs_part.relate_to(dst_slide_part, RT.SLIDE)
443
+ prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
444
+ sldId = prs_element.get_or_add_sldIdLst().add_sldId(rId)
445
+ # A sectioned destination keeps its section list a complete partition
446
+ # — the imported slide lands at the end of the deck, so it joins the
447
+ # final section (same rule as Slides.add_slide).
448
+ sectionLst = getattr(prs_element, "sectionLst", None)
449
+ if sectionLst is not None and sectionLst.section_lst:
450
+ sectionLst.section_lst[-1].add_sldId(sldId.id)
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Part-copy helpers
455
+ # ---------------------------------------------------------------------------
456
+
457
+
458
+ def _clone_part(src_part: Part, new_partname: PackURI, dst_package: Package) -> Part:
459
+ """Return a new Part in *dst_package* that is a copy of *src_part*.
460
+
461
+ For XmlParts the XML is re-parsed (no element sharing between packages).
462
+ For binary Parts the blob bytes are shared (immutable).
463
+ """
464
+ return PartFactory(new_partname, src_part.content_type, dst_package, blob=src_part.blob)
465
+
466
+
467
+ def _clone_xml_part(src_part: XmlPart, new_partname: PackURI, dst_package: Package) -> XmlPart:
468
+ """Return a new XmlPart in *dst_package* that is a copy of *src_part*.
469
+
470
+ The XML element tree is deep-copied so mutations on one part do not affect the other.
471
+ No relationships are copied; callers must add them explicitly.
472
+ """
473
+ new_element = deepcopy(src_part._element) # pyright: ignore[reportPrivateUsage]
474
+ return src_part.__class__(new_partname, src_part.content_type, dst_package, new_element)
475
+
476
+
477
+ # Relationship-id references embedded in part XML all live in the ``r:``
478
+ # namespace (``r:embed``, ``r:id``, ``r:link``, and the SmartArt
479
+ # ``dgm:relIds`` ``r:dm``/``r:lo``/``r:qs``/``r:cs``). When a part is cloned its
480
+ # relationships are re-created in copy order, so the new rIds rarely match the
481
+ # ones baked into the deep-copied XML — leaving e.g. a picture's
482
+ # ``r:embed="rId2"`` pointing at whatever part happened to land on rId2 in the
483
+ # destination. PowerPoint then can't resolve the image and repairs the deck.
484
+ _R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
485
+
486
+
487
+ def _remap_rids(element, id_map: dict[str, str]) -> None:
488
+ """Rewrite every ``r:*`` attribute on *element*'s tree through *id_map*.
489
+
490
+ Only relationship-namespace attributes whose value is a known source rId are
491
+ touched, so non-relationship attributes are never disturbed.
492
+ """
493
+ if not id_map:
494
+ return
495
+ prefix = "{%s}" % _R_NS
496
+ for el in element.iter():
497
+ for name, value in list(el.attrib.items()):
498
+ if name.startswith(prefix) and value in id_map:
499
+ el.set(name, id_map[value])
500
+
501
+
502
+ def _master_fingerprint(master_part: Part) -> bytes:
503
+ """Return a SHA-256 hash over the normalised master XML + its theme XML.
504
+
505
+ Used for master deduplication: two masters with identical fingerprints are
506
+ considered equivalent for the purposes of ``merge_master='dedupe'``.
507
+
508
+ The hash must be *stable across packages*: cloning a master rebuilds its
509
+ `p:sldLayoutIdLst` (fresh rIds and unique ids) and remaps any `r:embed`
510
+ references in its body, so those package-allocation artifacts are
511
+ normalised out before hashing. Otherwise a master cloned by one import
512
+ would stop matching its own source, and every later import of a slide
513
+ from that source would clone yet another duplicate master/layout set.
514
+ """
515
+ h = hashlib.sha256()
516
+ element = getattr(master_part, "_element", None)
517
+ if element is not None:
518
+ h.update(_normalized_master_xml(master_part, element))
519
+ else: # pragma: no cover - a master part is always an XmlPart
520
+ h.update(master_part.blob)
521
+ try:
522
+ theme_part = master_part.part_related_by(RT.THEME)
523
+ h.update(theme_part.blob)
524
+ except KeyError:
525
+ pass
526
+ return h.digest()
527
+
528
+
529
+ def _normalized_master_xml(master_part: Part, master_elm) -> bytes:
530
+ """Serialize *master_elm* with package-allocation artifacts normalised.
531
+
532
+ Drops the `p:sldLayoutIdLst` (its rId/id values are allocated per
533
+ package) and replaces every relationship-id attribute value (`r:embed`
534
+ etc. are renumbered when dependencies are copied) with a *stable token
535
+ derived from the referenced content* — the SHA-1 of the target part's
536
+ bytes, or the verbatim URL for an external target. Masking with a
537
+ constant would make two masters that differ only in a referenced image
538
+ (a different logo, say) fingerprint-identical and dedupe onto the wrong
539
+ branding; content-addressing keeps the hash stable across packages
540
+ while still distinguishing what the references point at.
541
+ """
542
+ from lxml import etree
543
+
544
+ p_ns = "http://schemas.openxmlformats.org/presentationml/2006/main"
545
+ clone = deepcopy(master_elm)
546
+ for layout_id_lst in clone.findall("{%s}sldLayoutIdLst" % p_ns):
547
+ clone.remove(layout_id_lst)
548
+
549
+ token_cache: dict[str, str] = {}
550
+
551
+ def _target_token(rId: str) -> str:
552
+ if rId in token_cache:
553
+ return token_cache[rId]
554
+ token = "unresolved"
555
+ try:
556
+ rel = master_part.rels[rId]
557
+ except KeyError:
558
+ rel = None
559
+ if rel is not None:
560
+ if rel.is_external:
561
+ token = "external:%s" % rel.target_ref
562
+ else:
563
+ token = hashlib.sha1(rel.target_part.blob).hexdigest()
564
+ token_cache[rId] = token
565
+ return token
566
+
567
+ r_prefix = "{%s}" % _R_NS
568
+ for el in clone.iter():
569
+ for attr_name, attr_value in list(el.attrib.items()):
570
+ if attr_name.startswith(r_prefix):
571
+ el.set(attr_name, _target_token(attr_value))
572
+ return etree.tostring(clone)
573
+
574
+
575
+ def _partname_template(partname: PackURI) -> str:
576
+ """Return a partname template string suitable for ``Package.next_partname()``.
577
+
578
+ E.g. ``/ppt/charts/chart3.xml`` → ``/ppt/charts/chart%d.xml``.
579
+ """
580
+ # Split off the trailing number (if any) and extension
581
+ name = partname # str-like
582
+ # Find the last digit sequence before the extension
583
+ base = name.rsplit(".", 1)
584
+ if len(base) == 2:
585
+ stem, ext = base
586
+ else:
587
+ stem, ext = name, ""
588
+
589
+ # Strip trailing digits from stem to get the "root"
590
+ root = stem.rstrip("0123456789")
591
+ if not root.endswith("/") and root == stem:
592
+ # No trailing digits — use the whole stem
593
+ root = stem
594
+
595
+ if ext:
596
+ return f"{root}%d.{ext}"
597
+ return f"{root}%d"