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.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/geometry.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Geometry primitives — ``BBox`` and friends.
|
|
2
|
+
|
|
3
|
+
A first-class value object for rectangular slide regions. EMU arithmetic
|
|
4
|
+
is verbose and error-prone; ``BBox`` does it for you::
|
|
5
|
+
|
|
6
|
+
from pptx2 import BBox, Inches
|
|
7
|
+
|
|
8
|
+
bb = BBox.from_inches(1, 2, 8, 4)
|
|
9
|
+
inner = bb.inset(all=Inches(0.2))
|
|
10
|
+
left, right = bb.split_h([1, 1], gap=Inches(0.1))
|
|
11
|
+
|
|
12
|
+
``BBox`` is immutable, hashable, and unpacks to
|
|
13
|
+
``(left, top, width, height)`` so it can be passed in place of the
|
|
14
|
+
historical 4-tuple to ``add_shape`` / ``add_textbox`` etc. when those
|
|
15
|
+
APIs accept ``*box``.
|
|
16
|
+
|
|
17
|
+
Every constructor returns a ``BBox`` whose four members are
|
|
18
|
+
:class:`~pptx2.util.Emu` instances — assigning ``bb.left`` to a
|
|
19
|
+
shape's ``shape.left`` works straight through.
|
|
20
|
+
|
|
21
|
+
See the parent skill notes for the full motivation; this module's
|
|
22
|
+
existence is item 1.3 of the v2.7-era recommendations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import TYPE_CHECKING, Iterable, Iterator, Sequence
|
|
29
|
+
|
|
30
|
+
from pptx2.util import Emu, Inches, Length, _coerce_emu
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from pptx2.shapes.base import BaseShape
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["BBox"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class BBox:
|
|
41
|
+
"""An immutable rectangular region in EMU.
|
|
42
|
+
|
|
43
|
+
``left``, ``top``, ``width``, ``height`` are integer EMU values
|
|
44
|
+
stored as :class:`~pptx2.util.Emu`. Negative dimensions are
|
|
45
|
+
rejected; negative positions are accepted (a shape can sit off the
|
|
46
|
+
slide).
|
|
47
|
+
|
|
48
|
+
Iteration yields ``(left, top, width, height)`` so the box can be
|
|
49
|
+
splatted into APIs that accept four positional length arguments::
|
|
50
|
+
|
|
51
|
+
bb = BBox.from_inches(1, 2, 4, 3)
|
|
52
|
+
rect = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *bb)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
left: Emu
|
|
56
|
+
top: Emu
|
|
57
|
+
width: Emu
|
|
58
|
+
height: Emu
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
# Coerce to Emu so callers can construct from plain ints / floats
|
|
62
|
+
# / Length subclasses without remembering the right wrapper.
|
|
63
|
+
# ``__setattr__`` is needed because the dataclass is frozen.
|
|
64
|
+
for name in ("left", "top", "width", "height"):
|
|
65
|
+
object.__setattr__(self, name, Emu(_coerce_emu(getattr(self, name))))
|
|
66
|
+
if int(self.width) < 0:
|
|
67
|
+
raise ValueError(f"BBox.width must be >= 0, got {int(self.width)}")
|
|
68
|
+
if int(self.height) < 0:
|
|
69
|
+
raise ValueError(f"BBox.height must be >= 0, got {int(self.height)}")
|
|
70
|
+
|
|
71
|
+
# -------------------------------------------------------------- constructors
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_shape(cls, shape: "BaseShape") -> "BBox":
|
|
75
|
+
"""Return the BBox of a shape (``left``, ``top``, ``width``, ``height``).
|
|
76
|
+
|
|
77
|
+
Works for any object exposing those four attributes — shapes,
|
|
78
|
+
pictures, group shapes, placeholders.
|
|
79
|
+
"""
|
|
80
|
+
return cls(shape.left, shape.top, shape.width, shape.height)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_inches(
|
|
84
|
+
cls, left: float, top: float, width: float, height: float
|
|
85
|
+
) -> "BBox":
|
|
86
|
+
"""Construct a BBox with positions in inches."""
|
|
87
|
+
return cls(Inches(left), Inches(top), Inches(width), Inches(height))
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_emu(
|
|
91
|
+
cls, left: int, top: int, width: int, height: int
|
|
92
|
+
) -> "BBox":
|
|
93
|
+
"""Construct a BBox with positions in raw EMU."""
|
|
94
|
+
return cls(Emu(left), Emu(top), Emu(width), Emu(height))
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_slide(cls, slide) -> "BBox":
|
|
98
|
+
"""Return the BBox covering the full slide area."""
|
|
99
|
+
prs = slide.part.package.presentation_part.presentation
|
|
100
|
+
return cls(Emu(0), Emu(0), Emu(int(prs.slide_width)), Emu(int(prs.slide_height)))
|
|
101
|
+
|
|
102
|
+
# -------------------------------------------------------------- properties
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def right(self) -> Emu:
|
|
106
|
+
"""X coordinate of the right edge (``left + width``)."""
|
|
107
|
+
return Emu(int(self.left) + int(self.width))
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def bottom(self) -> Emu:
|
|
111
|
+
"""Y coordinate of the bottom edge (``top + height``)."""
|
|
112
|
+
return Emu(int(self.top) + int(self.height))
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def cx(self) -> Emu:
|
|
116
|
+
"""Horizontal centre of the box."""
|
|
117
|
+
return Emu(int(self.left) + int(self.width) // 2)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def cy(self) -> Emu:
|
|
121
|
+
"""Vertical centre of the box."""
|
|
122
|
+
return Emu(int(self.top) + int(self.height) // 2)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def area(self) -> int:
|
|
126
|
+
return int(self.width) * int(self.height)
|
|
127
|
+
|
|
128
|
+
# -------------------------------------------------------------- iteration
|
|
129
|
+
|
|
130
|
+
def __iter__(self) -> Iterator[Emu]:
|
|
131
|
+
# Order matches the historical (left, top, width, height)
|
|
132
|
+
# positional argument convention used across add_shape, add_textbox,
|
|
133
|
+
# add_picture, etc.
|
|
134
|
+
yield self.left
|
|
135
|
+
yield self.top
|
|
136
|
+
yield self.width
|
|
137
|
+
yield self.height
|
|
138
|
+
|
|
139
|
+
def as_tuple(self) -> tuple[Emu, Emu, Emu, Emu]:
|
|
140
|
+
"""Return ``(left, top, width, height)``."""
|
|
141
|
+
return (self.left, self.top, self.width, self.height)
|
|
142
|
+
|
|
143
|
+
# -------------------------------------------------------------- transforms
|
|
144
|
+
|
|
145
|
+
def shifted(self, dx: int = 0, dy: int = 0) -> "BBox":
|
|
146
|
+
"""Return a new BBox translated by ``(dx, dy)`` EMU."""
|
|
147
|
+
return BBox(
|
|
148
|
+
Emu(int(self.left) + int(dx)),
|
|
149
|
+
Emu(int(self.top) + int(dy)),
|
|
150
|
+
self.width,
|
|
151
|
+
self.height,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def resized(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
width: int | None = None,
|
|
158
|
+
height: int | None = None,
|
|
159
|
+
) -> "BBox":
|
|
160
|
+
"""Return a new BBox with ``width`` and/or ``height`` overridden."""
|
|
161
|
+
return BBox(
|
|
162
|
+
self.left,
|
|
163
|
+
self.top,
|
|
164
|
+
Emu(int(width)) if width is not None else self.width,
|
|
165
|
+
Emu(int(height)) if height is not None else self.height,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def inset(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
all: int | None = None,
|
|
172
|
+
x: int | None = None,
|
|
173
|
+
y: int | None = None,
|
|
174
|
+
left: int | None = None,
|
|
175
|
+
top: int | None = None,
|
|
176
|
+
right: int | None = None,
|
|
177
|
+
bottom: int | None = None,
|
|
178
|
+
) -> "BBox":
|
|
179
|
+
"""Return a BBox shrunk inwards by the given padding amounts.
|
|
180
|
+
|
|
181
|
+
``all`` sets every side; ``x`` sets left+right; ``y`` sets
|
|
182
|
+
top+bottom; the four per-edge kwargs override anything else.
|
|
183
|
+
Defaults to zero — equivalent to a no-op when called with no args.
|
|
184
|
+
|
|
185
|
+
Negative values expand the box outward.
|
|
186
|
+
"""
|
|
187
|
+
l_in = _resolve_inset(left, x, all, 0)
|
|
188
|
+
r_in = _resolve_inset(right, x, all, 0)
|
|
189
|
+
t_in = _resolve_inset(top, y, all, 0)
|
|
190
|
+
b_in = _resolve_inset(bottom, y, all, 0)
|
|
191
|
+
new_left = int(self.left) + l_in
|
|
192
|
+
new_top = int(self.top) + t_in
|
|
193
|
+
new_w = int(self.width) - l_in - r_in
|
|
194
|
+
new_h = int(self.height) - t_in - b_in
|
|
195
|
+
if new_w < 0:
|
|
196
|
+
new_w = 0
|
|
197
|
+
if new_h < 0:
|
|
198
|
+
new_h = 0
|
|
199
|
+
return BBox(Emu(new_left), Emu(new_top), Emu(new_w), Emu(new_h))
|
|
200
|
+
|
|
201
|
+
def sub(self, fx: float, fy: float, fw: float, fh: float) -> "BBox":
|
|
202
|
+
"""Return a normalised sub-box (each arg in ``[0.0, 1.0]``).
|
|
203
|
+
|
|
204
|
+
``fx``/``fy`` are relative offsets, ``fw``/``fh`` are relative
|
|
205
|
+
widths/heights — so ``box.sub(0.25, 0, 0.5, 1.0)`` returns the
|
|
206
|
+
middle-half-width strip of the box.
|
|
207
|
+
"""
|
|
208
|
+
return BBox(
|
|
209
|
+
Emu(int(self.left) + int(round(fx * int(self.width)))),
|
|
210
|
+
Emu(int(self.top) + int(round(fy * int(self.height)))),
|
|
211
|
+
Emu(int(round(fw * int(self.width)))),
|
|
212
|
+
Emu(int(round(fh * int(self.height)))),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# -------------------------------------------------------------- splits
|
|
216
|
+
|
|
217
|
+
def split_h(
|
|
218
|
+
self, ratios: Sequence[float], gap: int = 0
|
|
219
|
+
) -> list["BBox"]:
|
|
220
|
+
"""Split horizontally into N columns with the given ratios.
|
|
221
|
+
|
|
222
|
+
``ratios=[1, 1]`` produces two equal columns; ``[2, 1]`` makes
|
|
223
|
+
the first column twice as wide as the second. ``gap`` is the
|
|
224
|
+
EMU gap between consecutive columns.
|
|
225
|
+
"""
|
|
226
|
+
return _split(self, ratios, gap, axis="h")
|
|
227
|
+
|
|
228
|
+
def split_v(
|
|
229
|
+
self, ratios: Sequence[float], gap: int = 0
|
|
230
|
+
) -> list["BBox"]:
|
|
231
|
+
"""Split vertically into N rows with the given ratios."""
|
|
232
|
+
return _split(self, ratios, gap, axis="v")
|
|
233
|
+
|
|
234
|
+
def columns(self, n: int, gap: int = 0) -> list["BBox"]:
|
|
235
|
+
"""Return `n` equal-width columns of this box, separated by `gap`.
|
|
236
|
+
|
|
237
|
+
The shorthand for the arithmetic every multi-column layout would
|
|
238
|
+
otherwise hand-roll (``col_w = (avail - (n - 1) * gap) / n`` and a
|
|
239
|
+
running cursor)::
|
|
240
|
+
|
|
241
|
+
for box, item in zip(row.columns(3, gap=Pt(16)), items):
|
|
242
|
+
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *box)
|
|
243
|
+
|
|
244
|
+
Widths are apportioned so the columns partition the box exactly — no
|
|
245
|
+
rounding drift on the last column. For unequal columns use
|
|
246
|
+
:meth:`split_h` with explicit ratios.
|
|
247
|
+
"""
|
|
248
|
+
return _split(self, [1] * _positive_count(n, "columns"), gap, axis="h")
|
|
249
|
+
|
|
250
|
+
def rows(self, n: int, gap: int = 0) -> list["BBox"]:
|
|
251
|
+
"""Return `n` equal-height rows of this box, separated by `gap`.
|
|
252
|
+
|
|
253
|
+
The vertical twin of :meth:`columns`; use :meth:`split_v` for
|
|
254
|
+
unequal rows.
|
|
255
|
+
"""
|
|
256
|
+
return _split(self, [1] * _positive_count(n, "rows"), gap, axis="v")
|
|
257
|
+
|
|
258
|
+
def grid(
|
|
259
|
+
self,
|
|
260
|
+
cols: int,
|
|
261
|
+
rows: int = 1,
|
|
262
|
+
*,
|
|
263
|
+
gap_x: int = 0,
|
|
264
|
+
gap_y: int = 0,
|
|
265
|
+
) -> list["BBox"]:
|
|
266
|
+
"""Return a flat list of ``cols * rows`` equal cells (row-major)."""
|
|
267
|
+
if cols < 1 or rows < 1:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"cols and rows must be >= 1; got cols={cols}, rows={rows}"
|
|
270
|
+
)
|
|
271
|
+
col_boxes = _split(self, [1] * cols, gap_x, axis="h")
|
|
272
|
+
# Pre-split each column into rows once; flatten in row-major order.
|
|
273
|
+
col_rows = [_split(cb, [1] * rows, gap_y, axis="v") for cb in col_boxes]
|
|
274
|
+
return [col_rows[c][r] for r in range(rows) for c in range(cols)]
|
|
275
|
+
|
|
276
|
+
# -------------------------------------------------------------- geometric
|
|
277
|
+
|
|
278
|
+
def contains(self, other: "BBox", *, tol: int = 0) -> bool:
|
|
279
|
+
"""True if ``other`` lies entirely inside this box (within ``tol``)."""
|
|
280
|
+
return (
|
|
281
|
+
int(other.left) >= int(self.left) - tol
|
|
282
|
+
and int(other.top) >= int(self.top) - tol
|
|
283
|
+
and int(other.right) <= int(self.right) + tol
|
|
284
|
+
and int(other.bottom) <= int(self.bottom) + tol
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def intersects(self, other: "BBox") -> bool:
|
|
288
|
+
"""True if this box overlaps ``other``.
|
|
289
|
+
|
|
290
|
+
Touching edges do *not* count as intersection — two boxes
|
|
291
|
+
sharing an edge or a corner return ``False``. This matches
|
|
292
|
+
the standard "overlap = shared area" interpretation.
|
|
293
|
+
"""
|
|
294
|
+
return not (
|
|
295
|
+
int(self.right) <= int(other.left)
|
|
296
|
+
or int(other.right) <= int(self.left)
|
|
297
|
+
or int(self.bottom) <= int(other.top)
|
|
298
|
+
or int(other.bottom) <= int(self.top)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def intersection(self, other: "BBox") -> "BBox":
|
|
302
|
+
"""Return the overlapping region, or a zero-area box if none."""
|
|
303
|
+
ix = max(int(self.left), int(other.left))
|
|
304
|
+
iy = max(int(self.top), int(other.top))
|
|
305
|
+
ix2 = min(int(self.right), int(other.right))
|
|
306
|
+
iy2 = min(int(self.bottom), int(other.bottom))
|
|
307
|
+
if ix2 < ix or iy2 < iy:
|
|
308
|
+
return BBox(Emu(ix), Emu(iy), Emu(0), Emu(0))
|
|
309
|
+
return BBox(Emu(ix), Emu(iy), Emu(ix2 - ix), Emu(iy2 - iy))
|
|
310
|
+
|
|
311
|
+
def union(self, other: "BBox") -> "BBox":
|
|
312
|
+
"""Return the smallest box enclosing both rectangles."""
|
|
313
|
+
ix = min(int(self.left), int(other.left))
|
|
314
|
+
iy = min(int(self.top), int(other.top))
|
|
315
|
+
ix2 = max(int(self.right), int(other.right))
|
|
316
|
+
iy2 = max(int(self.bottom), int(other.bottom))
|
|
317
|
+
return BBox(Emu(ix), Emu(iy), Emu(ix2 - ix), Emu(iy2 - iy))
|
|
318
|
+
|
|
319
|
+
# -------------------------------------------------------------- application
|
|
320
|
+
|
|
321
|
+
def apply_to(self, shape: "BaseShape") -> "BaseShape":
|
|
322
|
+
"""Set ``shape.left/top/width/height`` from this box."""
|
|
323
|
+
shape.left = self.left
|
|
324
|
+
shape.top = self.top
|
|
325
|
+
shape.width = self.width
|
|
326
|
+
shape.height = self.height
|
|
327
|
+
return shape
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _positive_count(n: int, name: str) -> int:
|
|
331
|
+
"""Return `n` as an int, rejecting counts below 1."""
|
|
332
|
+
count = int(n)
|
|
333
|
+
if count < 1:
|
|
334
|
+
raise ValueError(f"BBox.{name}() needs n >= 1, got {n!r}")
|
|
335
|
+
return count
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _resolve_inset(
|
|
339
|
+
edge: int | None,
|
|
340
|
+
axis: int | None,
|
|
341
|
+
all_: int | None,
|
|
342
|
+
default: int,
|
|
343
|
+
) -> int:
|
|
344
|
+
"""Combine the inset kwargs in order of specificity."""
|
|
345
|
+
if edge is not None:
|
|
346
|
+
return int(edge)
|
|
347
|
+
if axis is not None:
|
|
348
|
+
return int(axis)
|
|
349
|
+
if all_ is not None:
|
|
350
|
+
return int(all_)
|
|
351
|
+
return default
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _split(
|
|
355
|
+
box: BBox, ratios: Sequence[float], gap: int, *, axis: str
|
|
356
|
+
) -> list[BBox]:
|
|
357
|
+
if not ratios:
|
|
358
|
+
raise ValueError("ratios must not be empty")
|
|
359
|
+
if any(r < 0 for r in ratios):
|
|
360
|
+
raise ValueError("ratios must be non-negative")
|
|
361
|
+
total = float(sum(ratios))
|
|
362
|
+
if total <= 0:
|
|
363
|
+
raise ValueError("at least one ratio must be > 0")
|
|
364
|
+
|
|
365
|
+
if axis == "h":
|
|
366
|
+
span = int(box.width) - int(gap) * (len(ratios) - 1)
|
|
367
|
+
if span < 0:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"horizontal gaps ({len(ratios) - 1}×{int(gap)}) consume the "
|
|
370
|
+
f"entire box width ({int(box.width)} EMU)"
|
|
371
|
+
)
|
|
372
|
+
widths = _apportion(span, ratios, total)
|
|
373
|
+
out: list[BBox] = []
|
|
374
|
+
cursor = int(box.left)
|
|
375
|
+
for w in widths:
|
|
376
|
+
out.append(BBox(Emu(cursor), box.top, Emu(w), box.height))
|
|
377
|
+
cursor += w + int(gap)
|
|
378
|
+
return out
|
|
379
|
+
elif axis == "v":
|
|
380
|
+
span = int(box.height) - int(gap) * (len(ratios) - 1)
|
|
381
|
+
if span < 0:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"vertical gaps ({len(ratios) - 1}×{int(gap)}) consume the "
|
|
384
|
+
f"entire box height ({int(box.height)} EMU)"
|
|
385
|
+
)
|
|
386
|
+
heights = _apportion(span, ratios, total)
|
|
387
|
+
out2: list[BBox] = []
|
|
388
|
+
cursor = int(box.top)
|
|
389
|
+
for h in heights:
|
|
390
|
+
out2.append(BBox(box.left, Emu(cursor), box.width, Emu(h)))
|
|
391
|
+
cursor += h + int(gap)
|
|
392
|
+
return out2
|
|
393
|
+
else:
|
|
394
|
+
raise ValueError(f"axis must be 'h' or 'v'; got {axis!r}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _apportion(span: int, ratios: Sequence[float], total: float) -> list[int]:
|
|
398
|
+
"""Distribute *span* across *ratios* so the result sums to exactly *span*.
|
|
399
|
+
|
|
400
|
+
Rounding each segment independently with ``int(round(span * r / total))``
|
|
401
|
+
can drift up or down by a few EMU per split, which then accumulates across
|
|
402
|
+
nested ``BBox.grid`` calls and silently breaks layouts that assume the
|
|
403
|
+
cells partition the box exactly. Tracking the remaining span / remaining
|
|
404
|
+
ratio after each emission keeps the running total exact.
|
|
405
|
+
"""
|
|
406
|
+
widths: list[int] = []
|
|
407
|
+
rem_span = int(span)
|
|
408
|
+
rem_total = float(total)
|
|
409
|
+
for i, r in enumerate(ratios):
|
|
410
|
+
if i == len(ratios) - 1:
|
|
411
|
+
widths.append(rem_span)
|
|
412
|
+
break
|
|
413
|
+
if rem_total <= 0:
|
|
414
|
+
widths.append(0)
|
|
415
|
+
continue
|
|
416
|
+
w = int(round(rem_span * float(r) / rem_total))
|
|
417
|
+
widths.append(w)
|
|
418
|
+
rem_span -= w
|
|
419
|
+
rem_total -= float(r)
|
|
420
|
+
return widths
|
pptx2/inherit.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Theme-aware inheritance helpers for color/effect getters.
|
|
2
|
+
|
|
3
|
+
Phase 1 made every color getter non-mutating: reading a property that
|
|
4
|
+
isn't explicitly set returns ``None`` and leaves the XML untouched.
|
|
5
|
+
That preserves theme inheritance — but it also means callers who want
|
|
6
|
+
the *effective* color (the one PowerPoint will actually render) have to
|
|
7
|
+
walk the style hierarchy themselves.
|
|
8
|
+
|
|
9
|
+
This module provides the read-only resolver that closes that gap. The
|
|
10
|
+
public surface is a single function::
|
|
11
|
+
|
|
12
|
+
from pptx2.inherit import resolve_color
|
|
13
|
+
rgb = resolve_color(font.color, theme=prs.theme) # RGBColor or None
|
|
14
|
+
|
|
15
|
+
Scope (intentionally focused):
|
|
16
|
+
|
|
17
|
+
* ``MSO_COLOR_TYPE.RGB`` — returns the explicit ``RGBColor``, applying
|
|
18
|
+
the ``brightness`` adjustment if any.
|
|
19
|
+
* ``MSO_COLOR_TYPE.SCHEME`` — looks the theme color up via
|
|
20
|
+
``theme.colors[theme_color]`` and applies the brightness adjustment.
|
|
21
|
+
* ``None`` (no explicit color) — returns ``None`` without trying to
|
|
22
|
+
walk slide → layout → master placeholder inheritance. Implementing
|
|
23
|
+
the full placeholder walk is a substantial follow-up; this resolver
|
|
24
|
+
intentionally stops short of it so callers get a deterministic,
|
|
25
|
+
side-effect-free answer.
|
|
26
|
+
|
|
27
|
+
Brightness handling matches ``ColorFormat.brightness``: a value in
|
|
28
|
+
``[-1.0, 1.0]`` where negative numbers darken the resolved RGB
|
|
29
|
+
proportionally and positive numbers lighten it. The math mirrors
|
|
30
|
+
PowerPoint's ``lumMod``/``lumOff`` / ``tint``/``shade`` model closely
|
|
31
|
+
enough to be useful for design-system code that needs to render
|
|
32
|
+
mock-ups outside PowerPoint (e.g. the lint contrast check planned in
|
|
33
|
+
Phase 2).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import TYPE_CHECKING, Optional
|
|
39
|
+
|
|
40
|
+
from pptx2.dml.color import ColorFormat, RGBColor
|
|
41
|
+
from pptx2.enum.dml import MSO_COLOR_TYPE
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from pptx2.theme import Theme
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_color(color_format: ColorFormat, *, theme: "Theme | None" = None) -> Optional[RGBColor]:
|
|
48
|
+
"""Return the effective `RGBColor` for `color_format`, or ``None``.
|
|
49
|
+
|
|
50
|
+
`color_format` is any :class:`~pptx2.dml.color.ColorFormat` (including
|
|
51
|
+
the lazy proxy returned by ``Font.color`` / ``LineFormat.color``).
|
|
52
|
+
|
|
53
|
+
`theme` is required to resolve theme colors; pass
|
|
54
|
+
``presentation.theme``. When a scheme color cannot be resolved
|
|
55
|
+
(e.g. ``theme=None`` or the slot is unmapped) this function returns
|
|
56
|
+
``None`` rather than raising, so it composes safely with downstream
|
|
57
|
+
callers that fall back to a hard-coded default.
|
|
58
|
+
"""
|
|
59
|
+
color_type = color_format.type
|
|
60
|
+
if color_type == MSO_COLOR_TYPE.RGB:
|
|
61
|
+
rgb = color_format.rgb
|
|
62
|
+
if rgb is None:
|
|
63
|
+
return None
|
|
64
|
+
return _apply_brightness(rgb, color_format.brightness)
|
|
65
|
+
if color_type == MSO_COLOR_TYPE.SCHEME:
|
|
66
|
+
if theme is None:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
theme_color = color_format.theme_color
|
|
70
|
+
except AttributeError:
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
base = theme.colors[theme_color]
|
|
74
|
+
except (KeyError, TypeError):
|
|
75
|
+
return None
|
|
76
|
+
return _apply_brightness(base, color_format.brightness)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _apply_brightness(rgb: RGBColor, brightness: float | None) -> RGBColor:
|
|
81
|
+
"""Lighten/darken `rgb` toward white/black per PowerPoint's brightness model.
|
|
82
|
+
|
|
83
|
+
A `brightness` of 0 (or `None`) is a no-op. Positive values tint
|
|
84
|
+
toward white; negative values shade toward black. The blend ratio
|
|
85
|
+
is the absolute value of `brightness`, capped at 1.0.
|
|
86
|
+
"""
|
|
87
|
+
if not brightness:
|
|
88
|
+
return rgb
|
|
89
|
+
delta = max(-1.0, min(1.0, brightness))
|
|
90
|
+
if delta > 0:
|
|
91
|
+
return _blend(rgb, _WHITE, delta)
|
|
92
|
+
return _blend(rgb, _BLACK, -delta)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _blend(a: RGBColor, b: RGBColor, t: float) -> RGBColor:
|
|
96
|
+
"""Linear blend from `a` toward `b` by ratio `t` in ``[0, 1]``."""
|
|
97
|
+
return RGBColor(
|
|
98
|
+
_u8(a[0] + (b[0] - a[0]) * t),
|
|
99
|
+
_u8(a[1] + (b[1] - a[1]) * t),
|
|
100
|
+
_u8(a[2] + (b[2] - a[2]) * t),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _u8(value: float) -> int:
|
|
105
|
+
return max(0, min(255, int(round(value))))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
|
109
|
+
_BLACK = RGBColor(0x00, 0x00, 0x00)
|