athena-python-pptx 0.4.0__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.4.0 → athena_python_pptx-0.4.1}/PKG-INFO +1 -1
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/docs/API_PARITY_EXCEPTIONS.md +24 -13
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/client.py +1 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/commands.py +56 -54
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/__init__.py +9 -4
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/text/__init__.py +89 -6
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/typing.py +4 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pyproject.toml +1 -1
- athena_python_pptx-0.4.0/uv.lock +0 -1163
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/.gitignore +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/CHANGELOG.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/CLAUDE.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/README.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/_references.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/action.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/batching.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/errors.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/exc.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/media.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/package.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/shared.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/slide.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/slides.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/spec.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/table.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/types.py +0 -0
- {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.1}/pptx/units.py +0 -0
- {athena_python_pptx-0.4.0 → 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.4.
|
|
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
|
|
|
@@ -666,6 +666,7 @@ class Client:
|
|
|
666
666
|
flipV=transform_data.get("flipV"),
|
|
667
667
|
),
|
|
668
668
|
preview_text=elem_data.get("previewText"),
|
|
669
|
+
rich_content=elem_data.get("richContent"),
|
|
669
670
|
placeholder=placeholder,
|
|
670
671
|
properties=elem_data.get("properties"),
|
|
671
672
|
source=elem_data.get("source"),
|
|
@@ -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
|
|
@@ -2260,10 +2262,10 @@ class AddChart(Command):
|
|
|
2260
2262
|
f"chart_type must be one of {sorted(valid_types)}",
|
|
2261
2263
|
"chart_type",
|
|
2262
2264
|
)
|
|
2263
|
-
if self.w_emu
|
|
2264
|
-
raise ValidationError("w_emu must be
|
|
2265
|
-
if self.h_emu
|
|
2266
|
-
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")
|
|
2267
2269
|
if not self.series:
|
|
2268
2270
|
raise ValidationError("at least one series is required", "series")
|
|
2269
2271
|
if self.grouping is not None and self.grouping not in (
|
|
@@ -2576,10 +2578,10 @@ class AddChart2016(Command):
|
|
|
2576
2578
|
f"chart_type must be one of {sorted(valid)} (got {self.chart_type!r})",
|
|
2577
2579
|
"chart_type",
|
|
2578
2580
|
)
|
|
2579
|
-
if self.w_emu
|
|
2580
|
-
raise ValidationError("w_emu must be
|
|
2581
|
-
if self.h_emu
|
|
2582
|
-
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")
|
|
2583
2585
|
if not self.series:
|
|
2584
2586
|
raise ValidationError("at least one series is required", "series")
|
|
2585
2587
|
|
|
@@ -2952,10 +2954,10 @@ class AddMovie(Command):
|
|
|
2952
2954
|
def validate(self) -> None:
|
|
2953
2955
|
if self.slide_index < 0:
|
|
2954
2956
|
raise ValidationError("slide_index must be >= 0", "slide_index")
|
|
2955
|
-
if self.w_emu
|
|
2956
|
-
raise ValidationError("w_emu must be
|
|
2957
|
-
if self.h_emu
|
|
2958
|
-
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")
|
|
2959
2961
|
# Reject empty strings — they look "set" but ship no payload.
|
|
2960
2962
|
has_embedded = bool(self.movie_base64)
|
|
2961
2963
|
has_url = bool(self.url)
|
|
@@ -5331,6 +5331,7 @@ class Shape:
|
|
|
5331
5331
|
element_type: str = "text",
|
|
5332
5332
|
transform: Optional[Transform] = None,
|
|
5333
5333
|
preview_text: Optional[str] = None,
|
|
5334
|
+
rich_content: Optional[dict[str, Any]] = None,
|
|
5334
5335
|
properties: Optional[dict[str, Any]] = None,
|
|
5335
5336
|
placeholder: Optional[PlaceholderSnapshot] = None,
|
|
5336
5337
|
source: Optional[str] = None,
|
|
@@ -5349,7 +5350,10 @@ class Shape:
|
|
|
5349
5350
|
|
|
5350
5351
|
# Auto-shapes also support text in PowerPoint (phase-1 behavior).
|
|
5351
5352
|
if element_type in ("text", "shape"):
|
|
5352
|
-
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")
|
|
5353
5357
|
self._text_frame = TextFrame(
|
|
5354
5358
|
shape_id=shape_id,
|
|
5355
5359
|
buffer=buffer,
|
|
@@ -5357,6 +5361,7 @@ class Shape:
|
|
|
5357
5361
|
rich_content=rich_content,
|
|
5358
5362
|
text_frame_properties=text_frame_properties,
|
|
5359
5363
|
)
|
|
5364
|
+
self._text_frame._shape = self
|
|
5360
5365
|
|
|
5361
5366
|
# Known misspelled/non-existent attributes with specific fix guidance
|
|
5362
5367
|
_ATTRIBUTE_FIXES: dict[str, str] = {
|
|
@@ -7222,9 +7227,11 @@ class Shapes:
|
|
|
7222
7227
|
element_type=elem.type,
|
|
7223
7228
|
transform=elem.transform,
|
|
7224
7229
|
preview_text=elem.preview_text,
|
|
7230
|
+
rich_content=elem.rich_content,
|
|
7225
7231
|
properties=elem.properties,
|
|
7226
7232
|
placeholder=elem.placeholder,
|
|
7227
7233
|
source=elem.source,
|
|
7234
|
+
text_frame_properties=elem.text_frame_properties,
|
|
7228
7235
|
)
|
|
7229
7236
|
self._shapes_by_id[elem.id] = shape
|
|
7230
7237
|
is_inherited = shape._is_inherited
|
|
@@ -9259,7 +9266,6 @@ class Shapes:
|
|
|
9259
9266
|
properties=elem.properties,
|
|
9260
9267
|
placeholder=elem.placeholder,
|
|
9261
9268
|
source=elem.source,
|
|
9262
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9263
9269
|
)
|
|
9264
9270
|
elif elem.type == "connector":
|
|
9265
9271
|
shape = Connector(
|
|
@@ -9271,7 +9277,6 @@ class Shapes:
|
|
|
9271
9277
|
properties=elem.properties,
|
|
9272
9278
|
placeholder=elem.placeholder,
|
|
9273
9279
|
source=elem.source,
|
|
9274
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9275
9280
|
)
|
|
9276
9281
|
elif elem.type == "group":
|
|
9277
9282
|
shape = GroupShape(
|
|
@@ -9283,7 +9288,6 @@ class Shapes:
|
|
|
9283
9288
|
properties=elem.properties,
|
|
9284
9289
|
placeholder=elem.placeholder,
|
|
9285
9290
|
source=elem.source,
|
|
9286
|
-
text_frame_properties=elem.text_frame_properties,
|
|
9287
9291
|
)
|
|
9288
9292
|
else:
|
|
9289
9293
|
shape = Shape(
|
|
@@ -9293,6 +9297,7 @@ class Shapes:
|
|
|
9293
9297
|
element_type=elem.type,
|
|
9294
9298
|
transform=elem.transform,
|
|
9295
9299
|
preview_text=elem.preview_text,
|
|
9300
|
+
rich_content=elem.rich_content,
|
|
9296
9301
|
properties=elem.properties,
|
|
9297
9302
|
placeholder=elem.placeholder,
|
|
9298
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.4.
|
|
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"
|