athena-python-pptx 0.3.1__tar.gz → 0.4.1__tar.gz
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.
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/PKG-INFO +1 -1
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/docs/API_PARITY_EXCEPTIONS.md +24 -13
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/client.py +26 -12
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/commands.py +73 -54
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/__init__.py +54 -10
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/text/__init__.py +89 -6
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/typing.py +4 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pyproject.toml +1 -1
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/.gitignore +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/CHANGELOG.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/CLAUDE.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/README.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/_references.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/action.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/batching.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/errors.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/exc.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/media.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/package.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/shared.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/slide.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/slides.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/spec.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/table.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/types.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/units.py +0 -0
- {athena_python_pptx-0.3.1 → athena_python_pptx-0.4.1}/pptx/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
|
|
5
5
|
Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
|
|
6
6
|
Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
|
|
@@ -352,19 +352,30 @@ public reorder API (callers manipulate `slides.sldIdLst` XML directly).
|
|
|
352
352
|
prs.reorder_slides([2, 0, 1]) # move slide 2 to the front
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
-
###
|
|
356
|
-
|
|
357
|
-
`AddTextBox`, `AddShape`, `AddPicture`, `AddTable`,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
355
|
+
### Non-negative dimension validation on every `Add*` command
|
|
356
|
+
|
|
357
|
+
`AddTextBox`, `AddShape`, `AddPicture`, `AddTable`, `SetTransform`,
|
|
358
|
+
`AddOleObject`, `AddLinkedOleObject`, `AddLinkedTable`, `AddChart`,
|
|
359
|
+
`AddChart2016`, and `AddMovie` reject **negative** width/height
|
|
360
|
+
client-side (the server's Zod schema is `z.number().int().nonnegative()`).
|
|
361
|
+
|
|
362
|
+
**Zero is allowed**, matching python-pptx and OOXML — `a:ext` cx/cy are
|
|
363
|
+
`ST_PositiveCoordinate` whose XSD facet is `minInclusive="0"`, so a
|
|
364
|
+
zero-area shape (e.g. a degenerate divider line/connector) is valid and
|
|
365
|
+
python-pptx accepts it without complaint. Earlier SDK versions rejected
|
|
366
|
+
zero too; that was a parity departure that broke the common
|
|
367
|
+
"thin divider" idiom and aborted whole command batches when an agent
|
|
368
|
+
passed `height=0`, so it was reverted. A negative value still raises a
|
|
369
|
+
clear, field-named `ValidationError` locally (and `400` server-side)
|
|
370
|
+
rather than producing an invalid `<a:ext>`.
|
|
371
|
+
|
|
372
|
+
(For `AddPicture`, omit `width=`/`height=` entirely to fall back to the
|
|
373
|
+
image's native dimensions — that path is unchanged.)
|
|
374
|
+
|
|
375
|
+
`SetColWidth` and `SetRowHeight` also allow zero (`min(0)` server-side)
|
|
376
|
+
because zero means "auto-fit" for table dimensions. `SetPresentationSize`
|
|
377
|
+
still requires strictly positive dimensions (a zero-size slide is
|
|
378
|
+
degenerate, not a valid OOXML construct).
|
|
368
379
|
|
|
369
380
|
### Server fail-fast on partial batch failure
|
|
370
381
|
|
|
@@ -72,7 +72,11 @@ class Client:
|
|
|
72
72
|
base_url: Base URL of the API (e.g., "https://api.pptx-studio.com").
|
|
73
73
|
If not provided, uses ATHENA_PPTX_BASE_URL environment variable.
|
|
74
74
|
api_key: Optional API key for authentication.
|
|
75
|
-
If not provided,
|
|
75
|
+
If not provided, falls back to the ATHENA_PPTX_API_KEY
|
|
76
|
+
environment variable, then to ATHENA_API_KEY (the canonical
|
|
77
|
+
Athena user API key — same value, accepted as a fallback so
|
|
78
|
+
code running in sandboxes that only inject ATHENA_API_KEY
|
|
79
|
+
still authenticates).
|
|
76
80
|
timeout: Request timeout in seconds
|
|
77
81
|
|
|
78
82
|
Raises:
|
|
@@ -83,11 +87,13 @@ class Client:
|
|
|
83
87
|
base_url = os.environ.get("ATHENA_PPTX_BASE_URL")
|
|
84
88
|
if base_url is None:
|
|
85
89
|
raise ValueError(
|
|
86
|
-
"base_url must be provided or ATHENA_PPTX_BASE_URL
|
|
90
|
+
"base_url must be provided or the ATHENA_PPTX_BASE_URL "
|
|
91
|
+
"environment variable must be set. Example: "
|
|
92
|
+
'ATHENA_PPTX_BASE_URL="https://pptx-studio.prd.athenaintel.com".'
|
|
87
93
|
)
|
|
88
94
|
|
|
89
95
|
if api_key is None:
|
|
90
|
-
api_key = os.environ.get("ATHENA_PPTX_API_KEY")
|
|
96
|
+
api_key = os.environ.get("ATHENA_PPTX_API_KEY") or os.environ.get("ATHENA_API_KEY")
|
|
91
97
|
|
|
92
98
|
self.base_url = base_url.rstrip("/")
|
|
93
99
|
self.api_key = api_key
|
|
@@ -129,7 +135,18 @@ class Client:
|
|
|
129
135
|
def _handle_response(self, response: requests.Response) -> Any:
|
|
130
136
|
"""Handle API response, raising appropriate errors."""
|
|
131
137
|
if response.status_code == 401:
|
|
132
|
-
|
|
138
|
+
if not self.api_key:
|
|
139
|
+
raise AuthenticationError(
|
|
140
|
+
"No API key was sent. Pass api_key= to Presentation(...) "
|
|
141
|
+
"or set ATHENA_PPTX_API_KEY (or ATHENA_API_KEY as a "
|
|
142
|
+
"fallback) in the environment."
|
|
143
|
+
)
|
|
144
|
+
raise AuthenticationError(
|
|
145
|
+
"Invalid or expired API key. The pptx-studio API rejected the "
|
|
146
|
+
"credentials. Verify ATHENA_PPTX_API_KEY (or the ATHENA_API_KEY "
|
|
147
|
+
"fallback) is set to a valid Athena user API key for the "
|
|
148
|
+
"current workspace."
|
|
149
|
+
)
|
|
133
150
|
|
|
134
151
|
if response.status_code == 409:
|
|
135
152
|
data = response.json() if response.text else {}
|
|
@@ -416,7 +433,7 @@ class Client:
|
|
|
416
433
|
if name is None:
|
|
417
434
|
name = os.path.basename(file_path)
|
|
418
435
|
# Remove extension
|
|
419
|
-
if name.endswith(
|
|
436
|
+
if name.endswith(".pptx") or name.endswith(".potx"):
|
|
420
437
|
name = name[:-5]
|
|
421
438
|
|
|
422
439
|
# Step 1: Create deck to get presigned URL
|
|
@@ -496,7 +513,7 @@ class Client:
|
|
|
496
513
|
# Use filename as name if not provided
|
|
497
514
|
if name is None:
|
|
498
515
|
name = os.path.basename(file_path)
|
|
499
|
-
if name.endswith(
|
|
516
|
+
if name.endswith(".pptx") or name.endswith(".potx"):
|
|
500
517
|
name = name[:-5]
|
|
501
518
|
|
|
502
519
|
# Step 1: Create deck to get presigned URL
|
|
@@ -649,6 +666,7 @@ class Client:
|
|
|
649
666
|
flipV=transform_data.get("flipV"),
|
|
650
667
|
),
|
|
651
668
|
preview_text=elem_data.get("previewText"),
|
|
669
|
+
rich_content=elem_data.get("richContent"),
|
|
652
670
|
placeholder=placeholder,
|
|
653
671
|
properties=elem_data.get("properties"),
|
|
654
672
|
source=elem_data.get("source"),
|
|
@@ -800,9 +818,7 @@ class Client:
|
|
|
800
818
|
return self._download(download_url)
|
|
801
819
|
|
|
802
820
|
if status["status"] in ("failed", "error"):
|
|
803
|
-
raise ExportError(
|
|
804
|
-
status.get("error", "Export failed"), job_id
|
|
805
|
-
)
|
|
821
|
+
raise ExportError(status.get("error", "Export failed"), job_id)
|
|
806
822
|
|
|
807
823
|
time.sleep(poll_interval)
|
|
808
824
|
|
|
@@ -870,9 +886,7 @@ class Client:
|
|
|
870
886
|
return self._download(image_url)
|
|
871
887
|
|
|
872
888
|
if status["status"] in ("failed", "error"):
|
|
873
|
-
raise RenderError(
|
|
874
|
-
status.get("error", "Render failed"), job_id
|
|
875
|
-
)
|
|
889
|
+
raise RenderError(status.get("error", "Render failed"), job_id)
|
|
876
890
|
|
|
877
891
|
time.sleep(poll_interval)
|
|
878
892
|
|
|
@@ -87,14 +87,14 @@ class AddTextBox(Command):
|
|
|
87
87
|
def validate(self) -> None:
|
|
88
88
|
if self.slide_index < 0:
|
|
89
89
|
raise ValidationError("slide_index must be non-negative", "slide_index")
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
if self.w_emu
|
|
95
|
-
raise ValidationError(f"width must be
|
|
96
|
-
if self.h_emu
|
|
97
|
-
raise ValidationError(f"height must be
|
|
90
|
+
# OOXML `a:ext` cx/cy are ST_PositiveCoordinate (minInclusive=0), so
|
|
91
|
+
# zero-area shapes are valid (e.g. a degenerate divider/connector) and
|
|
92
|
+
# python-pptx accepts them. Only negatives are rejected, matching the
|
|
93
|
+
# server Zod schema (z.number().int().nonnegative()).
|
|
94
|
+
if self.w_emu < 0:
|
|
95
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
96
|
+
if self.h_emu < 0:
|
|
97
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
@dataclass
|
|
@@ -155,14 +155,15 @@ class SetTransform(Command):
|
|
|
155
155
|
def validate(self) -> None:
|
|
156
156
|
if not self.shape_id:
|
|
157
157
|
raise ValidationError("shape_id is required", "shape_id")
|
|
158
|
-
# Server Zod schema: wEmu / hEmu are z.number().int().
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
# Server Zod schema: wEmu / hEmu are z.number().int().nonnegative() —
|
|
159
|
+
# zero is valid (OOXML a:ext cx/cy are ST_PositiveCoordinate,
|
|
160
|
+
# minInclusive=0; python-pptx accepts zero-area shapes). Only negatives
|
|
161
|
+
# are rejected, mirrored locally so they surface synchronously with the
|
|
162
|
+
# offending field rather than failing server-side after the round-trip.
|
|
163
|
+
if self.w_emu is not None and self.w_emu < 0:
|
|
164
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
165
|
+
if self.h_emu is not None and self.h_emu < 0:
|
|
166
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
166
167
|
|
|
167
168
|
|
|
168
169
|
@dataclass
|
|
@@ -352,10 +353,10 @@ class AddShape(Command):
|
|
|
352
353
|
raise ValidationError("slide_index must be non-negative", "slide_index")
|
|
353
354
|
if not self.shape_type:
|
|
354
355
|
raise ValidationError("shape_type is required", "shape_type")
|
|
355
|
-
if self.w_emu
|
|
356
|
-
raise ValidationError(f"width must be
|
|
357
|
-
if self.h_emu
|
|
358
|
-
raise ValidationError(f"height must be
|
|
356
|
+
if self.w_emu < 0:
|
|
357
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
358
|
+
if self.h_emu < 0:
|
|
359
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
359
360
|
|
|
360
361
|
|
|
361
362
|
@dataclass
|
|
@@ -492,12 +493,13 @@ class AddPicture(Command):
|
|
|
492
493
|
raise ValidationError(
|
|
493
494
|
"image_format must be 'png', 'jpeg', 'gif', 'bmp', or 'tiff'", "image_format"
|
|
494
495
|
)
|
|
495
|
-
# Server schema treats wEmu / hEmu as
|
|
496
|
-
# them entirely to fall back to the image's native
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
496
|
+
# Server schema treats wEmu / hEmu as nonnegative() when present (zero
|
|
497
|
+
# is allowed; omit them entirely to fall back to the image's native
|
|
498
|
+
# size). Only negatives are rejected.
|
|
499
|
+
if self.w_emu is not None and self.w_emu < 0:
|
|
500
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
501
|
+
if self.h_emu is not None and self.h_emu < 0:
|
|
502
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
501
503
|
|
|
502
504
|
|
|
503
505
|
@dataclass
|
|
@@ -547,10 +549,10 @@ class AddOleObject(Command):
|
|
|
547
549
|
def validate(self) -> None:
|
|
548
550
|
if self.slide_index < 0:
|
|
549
551
|
raise ValidationError("slide_index must be non-negative", "slide_index")
|
|
550
|
-
if self.w_emu
|
|
551
|
-
raise ValidationError(f"width must be
|
|
552
|
-
if self.h_emu
|
|
553
|
-
raise ValidationError(f"height must be
|
|
552
|
+
if self.w_emu < 0:
|
|
553
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
554
|
+
if self.h_emu < 0:
|
|
555
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
554
556
|
if not self.preview_base64:
|
|
555
557
|
raise ValidationError("preview_base64 is required", "preview_base64")
|
|
556
558
|
if self.preview_format not in ("png", "jpeg"):
|
|
@@ -635,10 +637,10 @@ class AddLinkedOleObject(Command):
|
|
|
635
637
|
def validate(self) -> None:
|
|
636
638
|
if self.slide_index < 0:
|
|
637
639
|
raise ValidationError("slide_index must be non-negative", "slide_index")
|
|
638
|
-
if self.w_emu
|
|
639
|
-
raise ValidationError(f"width must be
|
|
640
|
-
if self.h_emu
|
|
641
|
-
raise ValidationError(f"height must be
|
|
640
|
+
if self.w_emu < 0:
|
|
641
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
642
|
+
if self.h_emu < 0:
|
|
643
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
642
644
|
if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
|
|
643
645
|
raise ValidationError(
|
|
644
646
|
"source_ref must be an AssetReference dict with an 'id' field",
|
|
@@ -730,10 +732,10 @@ class AddLinkedTable(Command):
|
|
|
730
732
|
def validate(self) -> None:
|
|
731
733
|
if self.slide_index < 0:
|
|
732
734
|
raise ValidationError("slide_index must be non-negative", "slide_index")
|
|
733
|
-
if self.w_emu
|
|
734
|
-
raise ValidationError(f"width must be
|
|
735
|
-
if self.h_emu
|
|
736
|
-
raise ValidationError(f"height must be
|
|
735
|
+
if self.w_emu < 0:
|
|
736
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
737
|
+
if self.h_emu < 0:
|
|
738
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
737
739
|
if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
|
|
738
740
|
raise ValidationError(
|
|
739
741
|
"source_ref must be an AssetReference dict with an 'id' field",
|
|
@@ -803,10 +805,10 @@ class AddTable(Command):
|
|
|
803
805
|
raise ValidationError("rows must be between 1 and 100", "rows")
|
|
804
806
|
if self.cols < 1 or self.cols > 26:
|
|
805
807
|
raise ValidationError("cols must be between 1 and 26", "cols")
|
|
806
|
-
if self.w_emu
|
|
807
|
-
raise ValidationError(f"width must be
|
|
808
|
-
if self.h_emu
|
|
809
|
-
raise ValidationError(f"height must be
|
|
808
|
+
if self.w_emu < 0:
|
|
809
|
+
raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
|
|
810
|
+
if self.h_emu < 0:
|
|
811
|
+
raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
|
|
810
812
|
|
|
811
813
|
|
|
812
814
|
@dataclass
|
|
@@ -1464,6 +1466,23 @@ class SetGradientFill(Command):
|
|
|
1464
1466
|
"stops"
|
|
1465
1467
|
)
|
|
1466
1468
|
|
|
1469
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1470
|
+
"""Serialize, camelCasing each stop's ``color_hex`` to ``colorHex`` for the server schema."""
|
|
1471
|
+
result = super().to_dict()
|
|
1472
|
+
if self.stops is not None:
|
|
1473
|
+
camel_stops: list[dict[str, Any]] = []
|
|
1474
|
+
for stop in self.stops:
|
|
1475
|
+
camel: dict[str, Any] = {
|
|
1476
|
+
"position": stop.get("position"),
|
|
1477
|
+
"colorHex": stop.get("color_hex"),
|
|
1478
|
+
}
|
|
1479
|
+
transparency = stop.get("transparency")
|
|
1480
|
+
if transparency is not None:
|
|
1481
|
+
camel["transparency"] = transparency
|
|
1482
|
+
camel_stops.append(camel)
|
|
1483
|
+
result["stops"] = camel_stops
|
|
1484
|
+
return result
|
|
1485
|
+
|
|
1467
1486
|
|
|
1468
1487
|
@dataclass
|
|
1469
1488
|
class SetShapeZOrder(Command):
|
|
@@ -2243,10 +2262,10 @@ class AddChart(Command):
|
|
|
2243
2262
|
f"chart_type must be one of {sorted(valid_types)}",
|
|
2244
2263
|
"chart_type",
|
|
2245
2264
|
)
|
|
2246
|
-
if self.w_emu
|
|
2247
|
-
raise ValidationError("w_emu must be
|
|
2248
|
-
if self.h_emu
|
|
2249
|
-
raise ValidationError("h_emu must be
|
|
2265
|
+
if self.w_emu < 0:
|
|
2266
|
+
raise ValidationError("w_emu must be >= 0", "w_emu")
|
|
2267
|
+
if self.h_emu < 0:
|
|
2268
|
+
raise ValidationError("h_emu must be >= 0", "h_emu")
|
|
2250
2269
|
if not self.series:
|
|
2251
2270
|
raise ValidationError("at least one series is required", "series")
|
|
2252
2271
|
if self.grouping is not None and self.grouping not in (
|
|
@@ -2559,10 +2578,10 @@ class AddChart2016(Command):
|
|
|
2559
2578
|
f"chart_type must be one of {sorted(valid)} (got {self.chart_type!r})",
|
|
2560
2579
|
"chart_type",
|
|
2561
2580
|
)
|
|
2562
|
-
if self.w_emu
|
|
2563
|
-
raise ValidationError("w_emu must be
|
|
2564
|
-
if self.h_emu
|
|
2565
|
-
raise ValidationError("h_emu must be
|
|
2581
|
+
if self.w_emu < 0:
|
|
2582
|
+
raise ValidationError("w_emu must be >= 0", "w_emu")
|
|
2583
|
+
if self.h_emu < 0:
|
|
2584
|
+
raise ValidationError("h_emu must be >= 0", "h_emu")
|
|
2566
2585
|
if not self.series:
|
|
2567
2586
|
raise ValidationError("at least one series is required", "series")
|
|
2568
2587
|
|
|
@@ -2935,10 +2954,10 @@ class AddMovie(Command):
|
|
|
2935
2954
|
def validate(self) -> None:
|
|
2936
2955
|
if self.slide_index < 0:
|
|
2937
2956
|
raise ValidationError("slide_index must be >= 0", "slide_index")
|
|
2938
|
-
if self.w_emu
|
|
2939
|
-
raise ValidationError("w_emu must be
|
|
2940
|
-
if self.h_emu
|
|
2941
|
-
raise ValidationError("h_emu must be
|
|
2957
|
+
if self.w_emu < 0:
|
|
2958
|
+
raise ValidationError("w_emu must be >= 0", "w_emu")
|
|
2959
|
+
if self.h_emu < 0:
|
|
2960
|
+
raise ValidationError("h_emu must be >= 0", "h_emu")
|
|
2942
2961
|
# Reject empty strings — they look "set" but ship no payload.
|
|
2943
2962
|
has_embedded = bool(self.movie_base64)
|
|
2944
2963
|
has_url = bool(self.url)
|
|
@@ -2963,6 +2963,43 @@ class _ChartPlotCollection:
|
|
|
2963
2963
|
# (rather than in their own file) so they're visible from the chart
|
|
2964
2964
|
# adapters above.
|
|
2965
2965
|
|
|
2966
|
+
# python-pptx ``XL_DATA_LABEL_POSITION`` (IntEnum) -> the OOXML ``dLblPos``
|
|
2967
|
+
# string symbols the server accepts. Assigning the enum member directly
|
|
2968
|
+
# (``data_labels.position = XL_LABEL_POSITION.OUTSIDE_END``) must serialize to
|
|
2969
|
+
# ``'outEnd'`` rather than the raw int ``2``, otherwise the commands endpoint
|
|
2970
|
+
# rejects the patch with a 400. The accepted set is mirrored from
|
|
2971
|
+
# ``packages/chart-core/src/schema.ts`` (``'ctr'|'inEnd'|'inBase'|'outEnd'|
|
|
2972
|
+
# 'bestFit'|'t'|'b'|'l'|'r'``). Mirrors the ``Legend._POSITION_INT_TO_STR``
|
|
2973
|
+
# pattern.
|
|
2974
|
+
_DATA_LABEL_POSITION_INT_TO_STR = {
|
|
2975
|
+
-4108: 'ctr', # CENTER
|
|
2976
|
+
3: 'inEnd', # INSIDE_END
|
|
2977
|
+
4: 'inBase', # INSIDE_BASE
|
|
2978
|
+
2: 'outEnd', # OUTSIDE_END
|
|
2979
|
+
5: 'bestFit', # BEST_FIT
|
|
2980
|
+
0: 't', # ABOVE
|
|
2981
|
+
1: 'b', # BELOW
|
|
2982
|
+
-4131: 'l', # LEFT
|
|
2983
|
+
-4152: 'r', # RIGHT
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
def _coerce_data_label_position(value: Any) -> Optional[str]:
|
|
2988
|
+
"""Normalize a data-label position to its OOXML ``dLblPos`` string.
|
|
2989
|
+
|
|
2990
|
+
Accepts an ``XL_DATA_LABEL_POSITION`` member (or raw int), in which case
|
|
2991
|
+
it maps to the OOXML symbol, or an already-valid string, which passes
|
|
2992
|
+
through unchanged. Unmappable ints resolve to ``None`` so no invalid
|
|
2993
|
+
patch is emitted (matching ``Legend.position``'s behavior)."""
|
|
2994
|
+
if value is None:
|
|
2995
|
+
return None
|
|
2996
|
+
if isinstance(value, bool):
|
|
2997
|
+
return None
|
|
2998
|
+
if isinstance(value, int):
|
|
2999
|
+
return _DATA_LABEL_POSITION_INT_TO_STR.get(int(value))
|
|
3000
|
+
return str(value)
|
|
3001
|
+
|
|
3002
|
+
|
|
2966
3003
|
class DataLabel:
|
|
2967
3004
|
"""python-pptx parity for a single data label on a chart series.
|
|
2968
3005
|
|
|
@@ -3047,9 +3084,10 @@ class DataLabel:
|
|
|
3047
3084
|
|
|
3048
3085
|
@position.setter
|
|
3049
3086
|
def position(self, value: Optional[str]) -> None:
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3087
|
+
coerced = _coerce_data_label_position(value)
|
|
3088
|
+
self._position = coerced
|
|
3089
|
+
if coerced is not None:
|
|
3090
|
+
self._emit(position=coerced)
|
|
3053
3091
|
|
|
3054
3092
|
@property
|
|
3055
3093
|
@athena_extension(
|
|
@@ -3357,9 +3395,10 @@ class DataLabels:
|
|
|
3357
3395
|
|
|
3358
3396
|
@position.setter
|
|
3359
3397
|
def position(self, value: Optional[str]) -> None:
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3398
|
+
coerced = _coerce_data_label_position(value)
|
|
3399
|
+
self._position = coerced
|
|
3400
|
+
if coerced is not None:
|
|
3401
|
+
self._emit_style(position=coerced)
|
|
3363
3402
|
|
|
3364
3403
|
@property
|
|
3365
3404
|
def show_category_name(self) -> bool:
|
|
@@ -5292,6 +5331,7 @@ class Shape:
|
|
|
5292
5331
|
element_type: str = "text",
|
|
5293
5332
|
transform: Optional[Transform] = None,
|
|
5294
5333
|
preview_text: Optional[str] = None,
|
|
5334
|
+
rich_content: Optional[dict[str, Any]] = None,
|
|
5295
5335
|
properties: Optional[dict[str, Any]] = None,
|
|
5296
5336
|
placeholder: Optional[PlaceholderSnapshot] = None,
|
|
5297
5337
|
source: Optional[str] = None,
|
|
@@ -5310,7 +5350,10 @@ class Shape:
|
|
|
5310
5350
|
|
|
5311
5351
|
# Auto-shapes also support text in PowerPoint (phase-1 behavior).
|
|
5312
5352
|
if element_type in ("text", "shape"):
|
|
5313
|
-
rich_content
|
|
5353
|
+
if rich_content is None and properties:
|
|
5354
|
+
# Older snapshot experiments nested richContent under
|
|
5355
|
+
# properties. Current API snapshots send it top-level.
|
|
5356
|
+
rich_content = properties.get("richContent")
|
|
5314
5357
|
self._text_frame = TextFrame(
|
|
5315
5358
|
shape_id=shape_id,
|
|
5316
5359
|
buffer=buffer,
|
|
@@ -5318,6 +5361,7 @@ class Shape:
|
|
|
5318
5361
|
rich_content=rich_content,
|
|
5319
5362
|
text_frame_properties=text_frame_properties,
|
|
5320
5363
|
)
|
|
5364
|
+
self._text_frame._shape = self
|
|
5321
5365
|
|
|
5322
5366
|
# Known misspelled/non-existent attributes with specific fix guidance
|
|
5323
5367
|
_ATTRIBUTE_FIXES: dict[str, str] = {
|
|
@@ -7183,9 +7227,11 @@ class Shapes:
|
|
|
7183
7227
|
element_type=elem.type,
|
|
7184
7228
|
transform=elem.transform,
|
|
7185
7229
|
preview_text=elem.preview_text,
|
|
7230
|
+
rich_content=elem.rich_content,
|
|
7186
7231
|
properties=elem.properties,
|
|
7187
7232
|
placeholder=elem.placeholder,
|
|
7188
7233
|
source=elem.source,
|
|
7234
|
+
text_frame_properties=elem.text_frame_properties,
|
|
7189
7235
|
)
|
|
7190
7236
|
self._shapes_by_id[elem.id] = shape
|
|
7191
7237
|
is_inherited = shape._is_inherited
|
|
@@ -9220,7 +9266,6 @@ class Shapes:
|
|
|
9220
9266
|
properties=elem.properties,
|
|
9221
9267
|
placeholder=elem.placeholder,
|
|
9222
9268
|
source=elem.source,
|
|
9223
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9224
9269
|
)
|
|
9225
9270
|
elif elem.type == "connector":
|
|
9226
9271
|
shape = Connector(
|
|
@@ -9232,7 +9277,6 @@ class Shapes:
|
|
|
9232
9277
|
properties=elem.properties,
|
|
9233
9278
|
placeholder=elem.placeholder,
|
|
9234
9279
|
source=elem.source,
|
|
9235
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9236
9280
|
)
|
|
9237
9281
|
elif elem.type == "group":
|
|
9238
9282
|
shape = GroupShape(
|
|
@@ -9244,7 +9288,6 @@ class Shapes:
|
|
|
9244
9288
|
properties=elem.properties,
|
|
9245
9289
|
placeholder=elem.placeholder,
|
|
9246
9290
|
source=elem.source,
|
|
9247
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9248
9291
|
)
|
|
9249
9292
|
else:
|
|
9250
9293
|
shape = Shape(
|
|
@@ -9254,6 +9297,7 @@ class Shapes:
|
|
|
9254
9297
|
element_type=elem.type,
|
|
9255
9298
|
transform=elem.transform,
|
|
9256
9299
|
preview_text=elem.preview_text,
|
|
9300
|
+
rich_content=elem.rich_content,
|
|
9257
9301
|
properties=elem.properties,
|
|
9258
9302
|
placeholder=elem.placeholder,
|
|
9259
9303
|
source=elem.source,
|
|
@@ -162,6 +162,7 @@ class Font:
|
|
|
162
162
|
size: Optional[int] = None,
|
|
163
163
|
name: Optional[str] = None,
|
|
164
164
|
color_hex: Optional[str] = None,
|
|
165
|
+
font_ref: Optional[str] = None,
|
|
165
166
|
spacing_pt: Optional[float] = None,
|
|
166
167
|
strike: Optional[bool] = None,
|
|
167
168
|
baseline: Optional[int] = None,
|
|
@@ -174,6 +175,7 @@ class Font:
|
|
|
174
175
|
self._underline = underline
|
|
175
176
|
self._size = size # In EMU
|
|
176
177
|
self._name = name
|
|
178
|
+
self._font_ref = font_ref
|
|
177
179
|
self._color_hex = color_hex
|
|
178
180
|
self._spacing_pt = spacing_pt # Character spacing in points
|
|
179
181
|
# OOXML theme color name (e.g. ``"accent1"``, ``"tx1"``) when the
|
|
@@ -258,6 +260,7 @@ class Font:
|
|
|
258
260
|
@name.setter
|
|
259
261
|
def name(self, value: str) -> None:
|
|
260
262
|
self._name = value
|
|
263
|
+
self._font_ref = None
|
|
261
264
|
self._emit_style_change()
|
|
262
265
|
|
|
263
266
|
@property
|
|
@@ -428,6 +431,8 @@ class Font:
|
|
|
428
431
|
style["underline"] = self._underline
|
|
429
432
|
if self._name is not None:
|
|
430
433
|
style["fontFamily"] = self._name
|
|
434
|
+
if self._font_ref is not None:
|
|
435
|
+
style["fontRef"] = self._font_ref
|
|
431
436
|
if self._spacing_pt is not None:
|
|
432
437
|
style["spacingPt"] = self._spacing_pt
|
|
433
438
|
if self._strike is not None:
|
|
@@ -471,6 +476,8 @@ class Font:
|
|
|
471
476
|
style["fontSizePt"] = self._size / EMU_PER_PT
|
|
472
477
|
if self._name is not None:
|
|
473
478
|
style["fontFamily"] = self._name
|
|
479
|
+
if self._font_ref is not None:
|
|
480
|
+
style["fontRef"] = self._font_ref
|
|
474
481
|
if self._color_hex is not None:
|
|
475
482
|
style["colorHex"] = self._color_hex
|
|
476
483
|
if self._theme_color_name is not None:
|
|
@@ -510,13 +517,13 @@ class Font:
|
|
|
510
517
|
``000000`` / ``ENGLISH_US (1033)`` for the Athena blank-template
|
|
511
518
|
baseline.
|
|
512
519
|
|
|
513
|
-
Returns a dict with keys ``name``, ``
|
|
514
|
-
``italic``, ``underline``, ``strike``, ``color_hex``
|
|
515
|
-
or scheme name), ``language_id`` (int).
|
|
520
|
+
Returns a dict with keys ``name``, ``font_ref``, ``size_pt``,
|
|
521
|
+
``bold``, ``italic``, ``underline``, ``strike``, ``color_hex``
|
|
522
|
+
(RGB hex or scheme name), ``language_id`` (int).
|
|
516
523
|
|
|
517
524
|
Closes python-pptx#1063, #765, #883.
|
|
518
525
|
"""
|
|
519
|
-
para = self._run._paragraph if self._run else None
|
|
526
|
+
para = self._run._paragraph if self._run is not None else None
|
|
520
527
|
tf = para._text_frame if para else None
|
|
521
528
|
shape = getattr(tf, "_shape", None) if tf else None
|
|
522
529
|
|
|
@@ -527,6 +534,9 @@ class Font:
|
|
|
527
534
|
return None
|
|
528
535
|
|
|
529
536
|
para_font = getattr(para, "font", None) if para is not None else None
|
|
537
|
+
paragraph_default: TextStyle = {}
|
|
538
|
+
if para is not None:
|
|
539
|
+
paragraph_default = getattr(para, "_default_run_style", {}) or {}
|
|
530
540
|
|
|
531
541
|
# Shape-level font default (rare; populated for placeholder
|
|
532
542
|
# snapshots that carry ``shape._properties["fontDefault"]``).
|
|
@@ -554,6 +564,8 @@ class Font:
|
|
|
554
564
|
size_pt = self._size / EMU_PER_PT
|
|
555
565
|
elif para_font is not None and getattr(para_font, "_size", None) is not None:
|
|
556
566
|
size_pt = para_font._size / EMU_PER_PT
|
|
567
|
+
elif paragraph_default.get("fontSizePt") is not None:
|
|
568
|
+
size_pt = float(paragraph_default["fontSizePt"])
|
|
557
569
|
elif shape_default.get("sizePt") is not None:
|
|
558
570
|
size_pt = float(shape_default["sizePt"])
|
|
559
571
|
|
|
@@ -561,31 +573,42 @@ class Font:
|
|
|
561
573
|
"name": _first_non_none(
|
|
562
574
|
self._name,
|
|
563
575
|
getattr(para_font, "_name", None) if para_font else None,
|
|
576
|
+
paragraph_default.get("fontFamily"),
|
|
564
577
|
shape_default.get("name"),
|
|
565
578
|
DEFAULTS["name"],
|
|
566
579
|
),
|
|
580
|
+
"font_ref": _first_non_none(
|
|
581
|
+
self._font_ref,
|
|
582
|
+
getattr(para_font, "_font_ref", None) if para_font else None,
|
|
583
|
+
paragraph_default.get("fontRef"),
|
|
584
|
+
shape_default.get("fontRef"),
|
|
585
|
+
),
|
|
567
586
|
"size_pt": _first_non_none(size_pt, DEFAULTS["size_pt"]),
|
|
568
587
|
"bold": _first_non_none(
|
|
569
588
|
self._bold,
|
|
570
589
|
getattr(para_font, "_bold", None) if para_font else None,
|
|
590
|
+
paragraph_default.get("bold"),
|
|
571
591
|
shape_default.get("bold"),
|
|
572
592
|
DEFAULTS["bold"],
|
|
573
593
|
),
|
|
574
594
|
"italic": _first_non_none(
|
|
575
595
|
self._italic,
|
|
576
596
|
getattr(para_font, "_italic", None) if para_font else None,
|
|
597
|
+
paragraph_default.get("italic"),
|
|
577
598
|
shape_default.get("italic"),
|
|
578
599
|
DEFAULTS["italic"],
|
|
579
600
|
),
|
|
580
601
|
"underline": _first_non_none(
|
|
581
602
|
self._underline,
|
|
582
603
|
getattr(para_font, "_underline", None) if para_font else None,
|
|
604
|
+
paragraph_default.get("underline"),
|
|
583
605
|
shape_default.get("underline"),
|
|
584
606
|
DEFAULTS["underline"],
|
|
585
607
|
),
|
|
586
608
|
"strike": _first_non_none(
|
|
587
609
|
self._strike,
|
|
588
610
|
getattr(para_font, "_strike", None) if para_font else None,
|
|
611
|
+
paragraph_default.get("strike"),
|
|
589
612
|
shape_default.get("strike"),
|
|
590
613
|
DEFAULTS["strike"],
|
|
591
614
|
),
|
|
@@ -594,13 +617,17 @@ class Font:
|
|
|
594
617
|
self._theme_color_name,
|
|
595
618
|
getattr(para_font, "_color_hex", None) if para_font else None,
|
|
596
619
|
getattr(para_font, "_theme_color_name", None) if para_font else None,
|
|
620
|
+
paragraph_default.get("colorHex"),
|
|
621
|
+
paragraph_default.get("schemeColor"),
|
|
597
622
|
shape_default.get("colorHex"),
|
|
623
|
+
shape_default.get("schemeColor"),
|
|
598
624
|
shape_default.get("themeColor"),
|
|
599
625
|
DEFAULTS["color_hex"],
|
|
600
626
|
),
|
|
601
627
|
"language_id": _first_non_none(
|
|
602
628
|
self._language_id,
|
|
603
629
|
getattr(para_font, "_language_id", None) if para_font else None,
|
|
630
|
+
paragraph_default.get("languageId"),
|
|
604
631
|
shape_default.get("languageId"),
|
|
605
632
|
DEFAULTS["language_id"],
|
|
606
633
|
),
|
|
@@ -810,6 +837,7 @@ class Run:
|
|
|
810
837
|
underline=style.get("underline") if style else None,
|
|
811
838
|
size=int(style.get("fontSizePt", 0) * 12700) if style and style.get("fontSizePt") else None,
|
|
812
839
|
name=style.get("fontFamily") if style else None,
|
|
840
|
+
font_ref=style.get("fontRef") if style else None,
|
|
813
841
|
color_hex=style.get("colorHex") if style else None,
|
|
814
842
|
spacing_pt=style.get("spacingPt") if style else None,
|
|
815
843
|
)
|
|
@@ -1013,6 +1041,7 @@ class Run:
|
|
|
1013
1041
|
'underline': self._font._underline,
|
|
1014
1042
|
'size_emu': self._font._size,
|
|
1015
1043
|
'font_name': self._font._name,
|
|
1044
|
+
'font_ref': self._font._font_ref,
|
|
1016
1045
|
'color_hex': self._font._color_hex,
|
|
1017
1046
|
}
|
|
1018
1047
|
|
|
@@ -1110,6 +1139,7 @@ class Paragraph:
|
|
|
1110
1139
|
space_after: Optional[int] = None,
|
|
1111
1140
|
margin_left: Optional[int] = None,
|
|
1112
1141
|
indent: Optional[int] = None,
|
|
1142
|
+
default_run_style: Optional[TextStyle] = None,
|
|
1113
1143
|
):
|
|
1114
1144
|
self._text_frame = text_frame
|
|
1115
1145
|
self._index = index
|
|
@@ -1123,6 +1153,7 @@ class Paragraph:
|
|
|
1123
1153
|
self._space_after = space_after
|
|
1124
1154
|
self._margin_left = margin_left
|
|
1125
1155
|
self._indent = indent
|
|
1156
|
+
self._default_run_style = default_run_style or {}
|
|
1126
1157
|
|
|
1127
1158
|
if runs:
|
|
1128
1159
|
for i, run_data in enumerate(runs):
|
|
@@ -1761,6 +1792,7 @@ class Paragraph:
|
|
|
1761
1792
|
'alignment': self._alignment,
|
|
1762
1793
|
'level': self._level,
|
|
1763
1794
|
'bullet': self._bullet,
|
|
1795
|
+
'default_run_style': self._default_run_style,
|
|
1764
1796
|
'run_count': len(self._runs),
|
|
1765
1797
|
'runs': [run.to_dict() for run in self._runs],
|
|
1766
1798
|
}
|
|
@@ -1831,6 +1863,7 @@ class TextFrame:
|
|
|
1831
1863
|
self._shape_id = shape_id
|
|
1832
1864
|
self._buffer = buffer
|
|
1833
1865
|
self._preview_text = preview_text or ""
|
|
1866
|
+
self._shape: Any | None = None
|
|
1834
1867
|
self._paragraphs: list[Paragraph] = []
|
|
1835
1868
|
|
|
1836
1869
|
# Seed internal text-frame-properties state from the source snapshot so
|
|
@@ -1860,7 +1893,21 @@ class TextFrame:
|
|
|
1860
1893
|
if rich_content and rich_content.get("paragraphs"):
|
|
1861
1894
|
for i, para_data in enumerate(rich_content["paragraphs"]):
|
|
1862
1895
|
self._paragraphs.append(
|
|
1863
|
-
Paragraph(
|
|
1896
|
+
Paragraph(
|
|
1897
|
+
self,
|
|
1898
|
+
i,
|
|
1899
|
+
runs=para_data.get("runs"),
|
|
1900
|
+
alignment=para_data.get("alignment"),
|
|
1901
|
+
level=para_data.get("level", 0),
|
|
1902
|
+
bullet=para_data.get("bullet"),
|
|
1903
|
+
bullet_color_hex=para_data.get("bulletColor"),
|
|
1904
|
+
line_spacing=para_data.get("lineSpacing"),
|
|
1905
|
+
space_before=para_data.get("spaceBeforeEmu"),
|
|
1906
|
+
space_after=para_data.get("spaceAfterEmu"),
|
|
1907
|
+
margin_left=para_data.get("marginLeftEmu"),
|
|
1908
|
+
indent=para_data.get("indentEmu"),
|
|
1909
|
+
default_run_style=para_data.get("defaultRunStyle"),
|
|
1910
|
+
)
|
|
1864
1911
|
)
|
|
1865
1912
|
elif preview_text:
|
|
1866
1913
|
# Split plain text into paragraphs
|
|
@@ -1943,6 +1990,20 @@ class TextFrame:
|
|
|
1943
1990
|
if len(para._runs) > 1:
|
|
1944
1991
|
needs_rich = True
|
|
1945
1992
|
break
|
|
1993
|
+
if (
|
|
1994
|
+
para._alignment is not None
|
|
1995
|
+
or bool(para._level)
|
|
1996
|
+
or para._bullet is not None
|
|
1997
|
+
or para._bullet_color_hex is not None
|
|
1998
|
+
or para._line_spacing is not None
|
|
1999
|
+
or para._space_before is not None
|
|
2000
|
+
or para._space_after is not None
|
|
2001
|
+
or para._margin_left is not None
|
|
2002
|
+
or para._indent is not None
|
|
2003
|
+
or bool(para._default_run_style)
|
|
2004
|
+
):
|
|
2005
|
+
needs_rich = True
|
|
2006
|
+
break
|
|
1946
2007
|
for run in para._runs:
|
|
1947
2008
|
if run._hyperlink._address:
|
|
1948
2009
|
needs_rich = True
|
|
@@ -1973,7 +2034,28 @@ class TextFrame:
|
|
|
1973
2034
|
# regress to zero runs and break later SetRunStyle calls.
|
|
1974
2035
|
if not runs:
|
|
1975
2036
|
runs.append({"text": ""})
|
|
1976
|
-
|
|
2037
|
+
paragraph_data: dict = {"runs": runs}
|
|
2038
|
+
if para._alignment is not None:
|
|
2039
|
+
paragraph_data["alignment"] = para._alignment
|
|
2040
|
+
if para._level:
|
|
2041
|
+
paragraph_data["level"] = para._level
|
|
2042
|
+
if para._bullet is not None:
|
|
2043
|
+
paragraph_data["bullet"] = para._bullet
|
|
2044
|
+
if para._bullet_color_hex is not None:
|
|
2045
|
+
paragraph_data["bulletColor"] = para._bullet_color_hex
|
|
2046
|
+
if para._line_spacing is not None:
|
|
2047
|
+
paragraph_data["lineSpacing"] = para._line_spacing
|
|
2048
|
+
if para._space_before is not None:
|
|
2049
|
+
paragraph_data["spaceBeforeEmu"] = para._space_before
|
|
2050
|
+
if para._space_after is not None:
|
|
2051
|
+
paragraph_data["spaceAfterEmu"] = para._space_after
|
|
2052
|
+
if para._margin_left is not None:
|
|
2053
|
+
paragraph_data["marginLeftEmu"] = para._margin_left
|
|
2054
|
+
if para._indent is not None:
|
|
2055
|
+
paragraph_data["indentEmu"] = para._indent
|
|
2056
|
+
if para._default_run_style:
|
|
2057
|
+
paragraph_data["defaultRunStyle"] = para._default_run_style
|
|
2058
|
+
paragraphs.append(paragraph_data)
|
|
1977
2059
|
|
|
1978
2060
|
return {"paragraphs": paragraphs}
|
|
1979
2061
|
|
|
@@ -2789,6 +2871,7 @@ class TextFrame:
|
|
|
2789
2871
|
'size_emu': run.font._size,
|
|
2790
2872
|
'size_pt': run.font.size_pt,
|
|
2791
2873
|
'font_name': run.font._name,
|
|
2874
|
+
'font_ref': run.font._font_ref,
|
|
2792
2875
|
'color_hex': run.font._color_hex,
|
|
2793
2876
|
})
|
|
2794
2877
|
return styled_runs
|
|
@@ -137,6 +137,9 @@ class TextStyle(TypedDict, total=False):
|
|
|
137
137
|
|
|
138
138
|
fontSizePt: float
|
|
139
139
|
fontFamily: str
|
|
140
|
+
# OOXML theme font reference (for example ``+mj-lt`` or ``+mn-lt``).
|
|
141
|
+
# When present, export preserves this over hardcoding fontFamily.
|
|
142
|
+
fontRef: str
|
|
140
143
|
bold: bool
|
|
141
144
|
italic: bool
|
|
142
145
|
underline: bool
|
|
@@ -206,6 +209,7 @@ class ElementSnapshot:
|
|
|
206
209
|
slide_id: SlideId
|
|
207
210
|
transform: Transform
|
|
208
211
|
preview_text: Optional[str] = None
|
|
212
|
+
rich_content: Optional[dict[str, Any]] = None
|
|
209
213
|
placeholder: Optional[PlaceholderSnapshot] = None
|
|
210
214
|
properties: Optional[dict[str, Any]] = None
|
|
211
215
|
source: Optional[Literal["ingested", "sdk", "layout", "master"]] = None
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-pptx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|