athena-python-pptx 0.1.74__tar.gz → 0.1.76__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.1.74 → athena_python_pptx-0.1.76}/CHANGELOG.md +71 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/PKG-INFO +1 -1
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/client.py +17 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/commands.py +7 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/__init__.py +78 -8
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/text/__init__.py +25 -1
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/typing.py +21 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/units.py +20 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pyproject.toml +1 -1
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/.gitignore +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/CLAUDE.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/README.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/docs/API_PARITY_EXCEPTIONS.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/action.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/batching.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/errors.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/exc.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/media.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/package.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/shared.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/slide.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/slides.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/spec.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/table.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/types.py +0 -0
- {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.76}/pptx/util.py +0 -0
|
@@ -2,6 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.76
|
|
6
|
+
|
|
7
|
+
**Table cell paragraph alignment now round-trips to OOXML (baseline-parity Gap A2).**
|
|
8
|
+
|
|
9
|
+
`cell.text_frame.paragraphs[0].alignment = PP_ALIGN.X` previously stored
|
|
10
|
+
the value locally for python-pptx parity but never reached the server.
|
|
11
|
+
The setter now translates `PP_PARAGRAPH_ALIGNMENT` enum members (and the
|
|
12
|
+
python-pptx lowercase string aliases) to the OOXML short form
|
|
13
|
+
(`l` / `ctr` / `r` / `just` / `dist` / `thaiDist` / `justLow`) and emits
|
|
14
|
+
a `SetTableCell` command carrying `paragraphAlignment`. The applier
|
|
15
|
+
writes it to every paragraph in the cell's text body; the export worker
|
|
16
|
+
emits a `SetTableCellText` intent whose paragraph alignment lands on
|
|
17
|
+
`<a:pPr algn="…">` in the exported PPTX.
|
|
18
|
+
|
|
19
|
+
Closes ~52 baseline-parity XML diffs against the 33 fixtures in
|
|
20
|
+
`pptx-studio/tests/baseline-parity/`.
|
|
21
|
+
|
|
22
|
+
## 0.1.75
|
|
23
|
+
|
|
24
|
+
**Critical bug fix — invisible text from `font.size = Pt(X)`.**
|
|
25
|
+
|
|
26
|
+
A user surfaced that agent-generated decks rendered as blank
|
|
27
|
+
rectangles in the Olympus editor. Investigation traced the symptom to
|
|
28
|
+
`fontSizePt: 355600` (= 28 × 12700) landing on the wire instead of
|
|
29
|
+
`fontSizePt: 28` — making the renderer treat text as 355,600-point
|
|
30
|
+
glyphs and rendering them effectively invisible.
|
|
31
|
+
|
|
32
|
+
### Root cause
|
|
33
|
+
|
|
34
|
+
`Length.__truediv__` returns a `Length` subclass — e.g.
|
|
35
|
+
`Pt(28) / EMU_PER_PT` returns a `Pt` instance with raw int value `28`
|
|
36
|
+
(meaning 28 EMU, semantically "28 points worth"). The SDK's font-size
|
|
37
|
+
serializer relies on this to convert EMU → points:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
style["fontSizePt"] = self._size / EMU_PER_PT # Pt(355600) / 12700
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The trouble: `Command.to_dict()` calls `dataclasses.asdict()` which
|
|
44
|
+
**deep-copies all nested values**. `copy.deepcopy(Pt(28))` reconstructs
|
|
45
|
+
via `Pt.__new__(28)`, and `Pt.__new__` interprets its argument as
|
|
46
|
+
*points* (not EMU) and multiplies by 12700. So a Pt instance whose
|
|
47
|
+
internal int was 28 EMU came back out as 28 × 12700 = 355600 EMU.
|
|
48
|
+
|
|
49
|
+
By the time the JSON went over the wire, `fontSizePt` was 355600.
|
|
50
|
+
Server stored it, renderer rendered text at 355,600pt → invisible.
|
|
51
|
+
|
|
52
|
+
### Fix
|
|
53
|
+
|
|
54
|
+
Override `Length.__reduce_ex__` so `copy.deepcopy` (and `pickle`)
|
|
55
|
+
reconstruct via the new module-level `_length_reconstruct`, which uses
|
|
56
|
+
`Length._from_emu` (preserves raw EMU value). Three lines in
|
|
57
|
+
`pptx/units.py`; behaviour-preserving for every existing call site.
|
|
58
|
+
|
|
59
|
+
### Regression test
|
|
60
|
+
|
|
61
|
+
New `test_length_round_trips_through_deepcopy` in `tests/test_units.py`
|
|
62
|
+
asserts that `copy.deepcopy(Pt(28))` and `asdict({...Pt(28) / 12700...})`
|
|
63
|
+
both preserve the raw EMU value. Would have caught this bug on day one.
|
|
64
|
+
|
|
65
|
+
### Impact
|
|
66
|
+
|
|
67
|
+
Every agent-authored deck where the agent set `run.font.size = Pt(X)`
|
|
68
|
+
through the SDK was silently shipping `fontSizePt: X * 12700` to the
|
|
69
|
+
server. Existing decks may need to be regenerated by the agent or
|
|
70
|
+
manually corrected. Future agent runs against v0.1.75+ will render
|
|
71
|
+
text correctly.
|
|
72
|
+
|
|
73
|
+
The new SDK was published to PyPI as 0.1.75 and the Daytona
|
|
74
|
+
`presentation-exec` snapshot bumped to v38.
|
|
75
|
+
|
|
5
76
|
## 0.1.74
|
|
6
77
|
|
|
7
78
|
**True drop-in import-path parity.** A behavioural-fidelity audit found
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.76
|
|
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
|
|
@@ -542,6 +542,7 @@ class Client:
|
|
|
542
542
|
MasterSnapshot,
|
|
543
543
|
PlaceholderSnapshot,
|
|
544
544
|
SlideSnapshot,
|
|
545
|
+
TextFramePropertiesSnapshot,
|
|
545
546
|
ThemeHierarchySnapshot,
|
|
546
547
|
Transform,
|
|
547
548
|
)
|
|
@@ -577,6 +578,21 @@ class Client:
|
|
|
577
578
|
local_transform_override=placeholder_data.get("localTransformOverride"),
|
|
578
579
|
)
|
|
579
580
|
|
|
581
|
+
# Server stores margins under `marginLeftEmu` after any SDK update
|
|
582
|
+
# and under `marginLeft` immediately after ingest. Read both.
|
|
583
|
+
tfp_data = elem_data.get("textFrameProperties")
|
|
584
|
+
text_frame_properties: Optional[TextFramePropertiesSnapshot] = None
|
|
585
|
+
if isinstance(tfp_data, dict):
|
|
586
|
+
text_frame_properties = TextFramePropertiesSnapshot(
|
|
587
|
+
word_wrap=tfp_data.get("wordWrap"),
|
|
588
|
+
auto_size=tfp_data.get("autoSize"),
|
|
589
|
+
vertical_anchor=tfp_data.get("verticalAnchor"),
|
|
590
|
+
margin_left=tfp_data.get("marginLeftEmu", tfp_data.get("marginLeft")),
|
|
591
|
+
margin_right=tfp_data.get("marginRightEmu", tfp_data.get("marginRight")),
|
|
592
|
+
margin_top=tfp_data.get("marginTopEmu", tfp_data.get("marginTop")),
|
|
593
|
+
margin_bottom=tfp_data.get("marginBottomEmu", tfp_data.get("marginBottom")),
|
|
594
|
+
)
|
|
595
|
+
|
|
580
596
|
elements[elem_id] = ElementSnapshot(
|
|
581
597
|
id=elem_data["id"],
|
|
582
598
|
type=elem_data["type"],
|
|
@@ -596,6 +612,7 @@ class Client:
|
|
|
596
612
|
source=elem_data.get("source"),
|
|
597
613
|
table_data=elem_data.get("tableData"),
|
|
598
614
|
asset_id=elem_data.get("assetId"),
|
|
615
|
+
text_frame_properties=text_frame_properties,
|
|
599
616
|
)
|
|
600
617
|
|
|
601
618
|
theme_hierarchy_raw = data.get("themeHierarchy")
|
|
@@ -476,6 +476,12 @@ class SetTableCell(Command):
|
|
|
476
476
|
col: Column index (0-based)
|
|
477
477
|
text: Cell text content
|
|
478
478
|
fill_color_hex: Cell background color
|
|
479
|
+
paragraph_alignment: Horizontal alignment of the cell's text body.
|
|
480
|
+
Mirrors python-pptx ``cell.text_frame.paragraphs[0].alignment``.
|
|
481
|
+
Accepts the OOXML short forms ``l``, ``ctr``, ``r``, ``just``,
|
|
482
|
+
``dist``, ``thaiDist``. Translated from ``PP_ALIGN`` /
|
|
483
|
+
``PP_PARAGRAPH_ALIGNMENT`` by the SDK before emit. Closes
|
|
484
|
+
baseline-parity Gap A2 (~52 diffs across table fixtures).
|
|
479
485
|
"""
|
|
480
486
|
|
|
481
487
|
shape_id: ShapeId
|
|
@@ -486,6 +492,7 @@ class SetTableCell(Command):
|
|
|
486
492
|
font_size_centipoints: Optional[int] = None
|
|
487
493
|
bold: Optional[bool] = None
|
|
488
494
|
font_color_hex: Optional[str] = None
|
|
495
|
+
paragraph_alignment: Optional[str] = None
|
|
489
496
|
|
|
490
497
|
@property
|
|
491
498
|
def command_type(self) -> str:
|
|
@@ -26,7 +26,7 @@ from ..chart.category import Categories
|
|
|
26
26
|
from ..dml.color import RGBColor, ColorFormat
|
|
27
27
|
from ..errors import UnsupportedFeatureError
|
|
28
28
|
from ..text import TextFrame
|
|
29
|
-
from ..typing import ElementSnapshot, ShapeId, Transform, PlaceholderSnapshot, PP_PLACEHOLDER
|
|
29
|
+
from ..typing import ElementSnapshot, ShapeId, Transform, PlaceholderSnapshot, PP_PLACEHOLDER, TextFramePropertiesSnapshot
|
|
30
30
|
from ..units import Emu, Length, ensure_emu
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
@@ -56,6 +56,57 @@ _ELEMENT_TYPE_TO_MSO_SHAPE_TYPE: dict[str, MSO_SHAPE_TYPE] = {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
# Map PP_PARAGRAPH_ALIGNMENT enum members and python-pptx string aliases
|
|
60
|
+
# to OOXML <a:pPr algn="..."> short forms. Accepts the IntEnum int values
|
|
61
|
+
# directly so callers can pass ``PP_ALIGN.CENTER`` without importing the
|
|
62
|
+
# enum class. ``None`` clears the attribute.
|
|
63
|
+
_PP_ALIGN_INT_TO_OOXML: dict[int, str] = {
|
|
64
|
+
1: "l",
|
|
65
|
+
2: "ctr",
|
|
66
|
+
3: "r",
|
|
67
|
+
4: "just",
|
|
68
|
+
5: "dist",
|
|
69
|
+
6: "thaiDist",
|
|
70
|
+
7: "justLow",
|
|
71
|
+
}
|
|
72
|
+
_PP_ALIGN_NAME_TO_OOXML: dict[str, str] = {
|
|
73
|
+
"left": "l",
|
|
74
|
+
"center": "ctr",
|
|
75
|
+
"right": "r",
|
|
76
|
+
"justify": "just",
|
|
77
|
+
"distribute": "dist",
|
|
78
|
+
"thai_distribute": "thaiDist",
|
|
79
|
+
"justify_low": "justLow",
|
|
80
|
+
# Already-short forms pass through.
|
|
81
|
+
"l": "l", "ctr": "ctr", "r": "r",
|
|
82
|
+
"just": "just", "dist": "dist", "thaiDist": "thaiDist", "justLow": "justLow",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_pp_align(value: Any) -> Optional[str]:
|
|
87
|
+
"""Translate a PP_ALIGN value to the OOXML short form used by SetTableCell.
|
|
88
|
+
|
|
89
|
+
Accepts ``PP_PARAGRAPH_ALIGNMENT`` (or any ``int``) and the lowercased
|
|
90
|
+
python-pptx string aliases used by ``pptx.text.PP_ALIGN``. Returns
|
|
91
|
+
``None`` for ``None`` input so the setter can clear the attribute.
|
|
92
|
+
"""
|
|
93
|
+
if value is None:
|
|
94
|
+
return None
|
|
95
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
96
|
+
try:
|
|
97
|
+
return _PP_ALIGN_INT_TO_OOXML[int(value)]
|
|
98
|
+
except KeyError:
|
|
99
|
+
raise ValueError(f"Unsupported paragraph alignment value: {value!r}")
|
|
100
|
+
if isinstance(value, str):
|
|
101
|
+
try:
|
|
102
|
+
return _PP_ALIGN_NAME_TO_OOXML[value]
|
|
103
|
+
except KeyError:
|
|
104
|
+
raise ValueError(f"Unsupported paragraph alignment value: {value!r}")
|
|
105
|
+
raise TypeError(
|
|
106
|
+
f"alignment must be a PP_ALIGN enum, int, or string; got {type(value).__name__}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
59
110
|
# Map XL_CHART_TYPE → (server chart_type, grouping). Only the chart types
|
|
60
111
|
# author.ts supports today are listed here; anything else raises
|
|
61
112
|
# UnsupportedFeatureError. Surface area mirrors python-pptx's accepted enum
|
|
@@ -2516,6 +2567,7 @@ class Shape:
|
|
|
2516
2567
|
properties: Optional[dict[str, Any]] = None,
|
|
2517
2568
|
placeholder: Optional[PlaceholderSnapshot] = None,
|
|
2518
2569
|
source: Optional[str] = None,
|
|
2570
|
+
text_frame_properties: Optional[TextFramePropertiesSnapshot] = None,
|
|
2519
2571
|
):
|
|
2520
2572
|
self._shape_id = shape_id
|
|
2521
2573
|
self._slide = slide
|
|
@@ -2536,6 +2588,7 @@ class Shape:
|
|
|
2536
2588
|
buffer=buffer,
|
|
2537
2589
|
preview_text=preview_text,
|
|
2538
2590
|
rich_content=rich_content,
|
|
2591
|
+
text_frame_properties=text_frame_properties,
|
|
2539
2592
|
)
|
|
2540
2593
|
|
|
2541
2594
|
# Known misspelled/non-existent attributes with specific fix guidance
|
|
@@ -5159,6 +5212,7 @@ class Shapes:
|
|
|
5159
5212
|
properties=elem.properties,
|
|
5160
5213
|
placeholder=elem.placeholder,
|
|
5161
5214
|
source=elem.source,
|
|
5215
|
+
text_frame_properties=elem.text_frame_properties,
|
|
5162
5216
|
)
|
|
5163
5217
|
elif elem.type == "connector":
|
|
5164
5218
|
shape = Connector(
|
|
@@ -5170,6 +5224,7 @@ class Shapes:
|
|
|
5170
5224
|
properties=elem.properties,
|
|
5171
5225
|
placeholder=elem.placeholder,
|
|
5172
5226
|
source=elem.source,
|
|
5227
|
+
text_frame_properties=elem.text_frame_properties,
|
|
5173
5228
|
)
|
|
5174
5229
|
elif elem.type == "group":
|
|
5175
5230
|
shape = GroupShape(
|
|
@@ -5181,6 +5236,7 @@ class Shapes:
|
|
|
5181
5236
|
properties=elem.properties,
|
|
5182
5237
|
placeholder=elem.placeholder,
|
|
5183
5238
|
source=elem.source,
|
|
5239
|
+
text_frame_properties=elem.text_frame_properties,
|
|
5184
5240
|
)
|
|
5185
5241
|
else:
|
|
5186
5242
|
shape = Shape(
|
|
@@ -5193,6 +5249,7 @@ class Shapes:
|
|
|
5193
5249
|
properties=elem.properties,
|
|
5194
5250
|
placeholder=elem.placeholder,
|
|
5195
5251
|
source=elem.source,
|
|
5252
|
+
text_frame_properties=elem.text_frame_properties,
|
|
5196
5253
|
)
|
|
5197
5254
|
self._shapes.append(shape)
|
|
5198
5255
|
self._shapes_by_id[elem.id] = shape
|
|
@@ -5808,15 +5865,13 @@ class _TableCellParagraph:
|
|
|
5808
5865
|
For API parity with python-pptx, ``add_run()`` and ``clear()`` are
|
|
5809
5866
|
supported but operate on the single underlying run — multiple
|
|
5810
5867
|
``add_run()`` calls return the *same* ``_TableCellRun`` object, and
|
|
5811
|
-
only the last text/style assignment wins. ``alignment``
|
|
5812
|
-
|
|
5813
|
-
paragraph-alignment command on the applier).
|
|
5868
|
+
only the last text/style assignment wins. ``alignment`` round-trips
|
|
5869
|
+
to ``<a:pPr algn="...">`` on the cell's text body via SetTableCell.
|
|
5814
5870
|
"""
|
|
5815
5871
|
|
|
5816
5872
|
def __init__(self, cell: "TableCell"):
|
|
5817
5873
|
self._cell = cell
|
|
5818
5874
|
self._runs = [_TableCellRun(cell)]
|
|
5819
|
-
self._alignment: Optional[Any] = None
|
|
5820
5875
|
|
|
5821
5876
|
@property
|
|
5822
5877
|
def runs(self) -> list[_TableCellRun]:
|
|
@@ -5832,12 +5887,22 @@ class _TableCellParagraph:
|
|
|
5832
5887
|
|
|
5833
5888
|
@property
|
|
5834
5889
|
def alignment(self) -> Optional[Any]:
|
|
5835
|
-
|
|
5890
|
+
"""Return the cell's paragraph alignment as the original input value.
|
|
5891
|
+
|
|
5892
|
+
Stored on the parent cell so that text-body re-creation (which
|
|
5893
|
+
rebuilds the paragraph wrapper) doesn't lose the assignment.
|
|
5894
|
+
"""
|
|
5895
|
+
return self._cell._paragraph_alignment_input
|
|
5836
5896
|
|
|
5837
5897
|
@alignment.setter
|
|
5838
5898
|
def alignment(self, value: Any) -> None:
|
|
5839
|
-
#
|
|
5840
|
-
|
|
5899
|
+
# Translate PP_ALIGN enum / string to OOXML short form. Stored on
|
|
5900
|
+
# the cell so we can re-emit it inside SetTableCell. Keep the
|
|
5901
|
+
# original input around for the getter to return per python-pptx
|
|
5902
|
+
# semantics (callers expect the same value type back).
|
|
5903
|
+
self._cell._paragraph_alignment_input = value
|
|
5904
|
+
self._cell._paragraph_alignment = _normalize_pp_align(value)
|
|
5905
|
+
self._cell._emit_cell_change()
|
|
5841
5906
|
|
|
5842
5907
|
def add_run(self) -> _TableCellRun:
|
|
5843
5908
|
"""Return the single underlying run.
|
|
@@ -5937,6 +6002,10 @@ class TableCell:
|
|
|
5937
6002
|
self._margin_top = margin_top
|
|
5938
6003
|
self._margin_bottom = margin_bottom
|
|
5939
6004
|
self._vertical_anchor = vertical_anchor
|
|
6005
|
+
# OOXML short form (``l``/``ctr``/``r``/...) sent over the wire and
|
|
6006
|
+
# the caller's original input retained for the python-pptx getter.
|
|
6007
|
+
self._paragraph_alignment: Optional[str] = None
|
|
6008
|
+
self._paragraph_alignment_input: Any = None
|
|
5940
6009
|
|
|
5941
6010
|
@property
|
|
5942
6011
|
def text(self) -> str:
|
|
@@ -6312,6 +6381,7 @@ class TableCell:
|
|
|
6312
6381
|
font_size_centipoints=int(self._font_size_pt * 100) if self._font_size_pt is not None else None,
|
|
6313
6382
|
bold=self._bold,
|
|
6314
6383
|
font_color_hex=self._font_color_hex,
|
|
6384
|
+
paragraph_alignment=self._paragraph_alignment,
|
|
6315
6385
|
)
|
|
6316
6386
|
self._table._buffer.add(cmd)
|
|
6317
6387
|
|
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
|
|
|
10
10
|
from ..commands import SetText, SetRunStyle, SetParagraphStyle, SetTextFrameProperties, FitText
|
|
11
11
|
from ..dml.color import RGBColor, ColorFormat
|
|
12
12
|
from ..errors import UnsupportedFeatureError
|
|
13
|
-
from ..typing import ShapeId, TextStyle
|
|
13
|
+
from ..typing import ShapeId, TextStyle, TextFramePropertiesSnapshot
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from ..batching import CommandBuffer
|
|
@@ -1328,12 +1328,36 @@ class TextFrame:
|
|
|
1328
1328
|
buffer: Optional[CommandBuffer],
|
|
1329
1329
|
preview_text: Optional[str] = None,
|
|
1330
1330
|
rich_content: Optional[dict[str, Any]] = None,
|
|
1331
|
+
text_frame_properties: Optional[TextFramePropertiesSnapshot] = None,
|
|
1331
1332
|
):
|
|
1332
1333
|
self._shape_id = shape_id
|
|
1333
1334
|
self._buffer = buffer
|
|
1334
1335
|
self._preview_text = preview_text or ""
|
|
1335
1336
|
self._paragraphs: list[Paragraph] = []
|
|
1336
1337
|
|
|
1338
|
+
# Seed internal text-frame-properties state from the source snapshot so
|
|
1339
|
+
# subsequent setter emits (which always rebroadcast the full state)
|
|
1340
|
+
# preserve the props the user hasn't touched. Without this, setting
|
|
1341
|
+
# `text_frame.margin_left = 0` would emit a SetTextFrameProperties
|
|
1342
|
+
# command without `autoSize`, the server's existing autoSize would be
|
|
1343
|
+
# the only thing keeping the box auto-fit — and it isn't there for
|
|
1344
|
+
# shapes created via AddTextBox, so the box renders without auto-fit.
|
|
1345
|
+
if text_frame_properties is not None:
|
|
1346
|
+
if text_frame_properties.word_wrap is not None:
|
|
1347
|
+
self._word_wrap = text_frame_properties.word_wrap
|
|
1348
|
+
if text_frame_properties.auto_size is not None:
|
|
1349
|
+
self._auto_size = text_frame_properties.auto_size
|
|
1350
|
+
if text_frame_properties.vertical_anchor is not None:
|
|
1351
|
+
self._vertical_anchor = text_frame_properties.vertical_anchor
|
|
1352
|
+
if text_frame_properties.margin_left is not None:
|
|
1353
|
+
self._margin_left = text_frame_properties.margin_left
|
|
1354
|
+
if text_frame_properties.margin_right is not None:
|
|
1355
|
+
self._margin_right = text_frame_properties.margin_right
|
|
1356
|
+
if text_frame_properties.margin_top is not None:
|
|
1357
|
+
self._margin_top = text_frame_properties.margin_top
|
|
1358
|
+
if text_frame_properties.margin_bottom is not None:
|
|
1359
|
+
self._margin_bottom = text_frame_properties.margin_bottom
|
|
1360
|
+
|
|
1337
1361
|
# Initialize paragraphs from rich content or plain text
|
|
1338
1362
|
if rich_content and rich_content.get("paragraphs"):
|
|
1339
1363
|
for i, para_data in enumerate(rich_content["paragraphs"]):
|
|
@@ -162,6 +162,26 @@ class PlaceholderSnapshot:
|
|
|
162
162
|
local_transform_override: Optional[bool] = None
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
@dataclass
|
|
166
|
+
class TextFramePropertiesSnapshot:
|
|
167
|
+
"""Snapshot of a text frame's structural properties (auto-size, margins, etc.).
|
|
168
|
+
|
|
169
|
+
Carries the source PPTX's <a:bodyPr> state through `from_url` so the SDK's
|
|
170
|
+
TextFrame can populate its internal `_auto_size`/`_word_wrap`/`_margin_*`
|
|
171
|
+
fields. Without this, a user setter (`text_frame.margin_left = 0`) emits a
|
|
172
|
+
SetTextFrameProperties command that omits the un-touched props, the server
|
|
173
|
+
keeps whatever it has, and round-trip is lossy.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
word_wrap: Optional[bool] = None
|
|
177
|
+
auto_size: Optional[Literal["none", "shape_to_fit_text", "text_to_fit_shape"]] = None
|
|
178
|
+
vertical_anchor: Optional[Literal["top", "middle", "bottom"]] = None
|
|
179
|
+
margin_left: Optional[int] = None
|
|
180
|
+
margin_right: Optional[int] = None
|
|
181
|
+
margin_top: Optional[int] = None
|
|
182
|
+
margin_bottom: Optional[int] = None
|
|
183
|
+
|
|
184
|
+
|
|
165
185
|
@dataclass
|
|
166
186
|
class ElementSnapshot:
|
|
167
187
|
"""Snapshot of an element's state."""
|
|
@@ -176,6 +196,7 @@ class ElementSnapshot:
|
|
|
176
196
|
source: Optional[Literal["ingested", "sdk", "layout", "master"]] = None
|
|
177
197
|
table_data: Optional[dict[str, Any]] = None
|
|
178
198
|
asset_id: Optional[str] = None
|
|
199
|
+
text_frame_properties: Optional[TextFramePropertiesSnapshot] = None
|
|
179
200
|
|
|
180
201
|
|
|
181
202
|
@dataclass
|
|
@@ -31,6 +31,17 @@ EMU_PER_CENTIPOINT = 127
|
|
|
31
31
|
EMU_PER_PX = 9525 # At 96 DPI
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _length_reconstruct(cls: type, emu_value: int): # noqa: ANN201
|
|
35
|
+
"""Top-level reconstructor for :class:`Length` subclasses.
|
|
36
|
+
|
|
37
|
+
Used by :meth:`Length.__reduce_ex__` so that ``copy.deepcopy`` (and
|
|
38
|
+
``pickle``) round-trip a :class:`Length` subclass without going
|
|
39
|
+
through the unit-converting ``__new__``. Must be a module-level
|
|
40
|
+
function so pickle can locate it by qualified name.
|
|
41
|
+
"""
|
|
42
|
+
return cls._from_emu(emu_value)
|
|
43
|
+
|
|
44
|
+
|
|
34
45
|
class Length(int):
|
|
35
46
|
"""
|
|
36
47
|
Length expressed in English Metric Units (EMU) — base unit class.
|
|
@@ -45,6 +56,15 @@ class Length(int):
|
|
|
45
56
|
def __new__(cls, value: Union[int, float, "Length"]) -> "Length":
|
|
46
57
|
return super().__new__(cls, int(round(value)))
|
|
47
58
|
|
|
59
|
+
# ``copy.deepcopy`` (used by ``dataclasses.asdict``) reconstructs ints
|
|
60
|
+
# by invoking ``Type(int_value)``. For our subclasses that means
|
|
61
|
+
# ``Pt(355600)`` etc., which interpret the value as user-units and
|
|
62
|
+
# multiply by the unit's EMU factor → ``355600 → 4,516,120,000``.
|
|
63
|
+
# Override the pickle/reduce protocol so deep-copy round-trips via
|
|
64
|
+
# ``_from_emu`` (treats the value as raw EMU), preserving identity.
|
|
65
|
+
def __reduce_ex__(self, protocol: int): # type: ignore[override]
|
|
66
|
+
return (_length_reconstruct, (type(self), int(self)))
|
|
67
|
+
|
|
48
68
|
@property
|
|
49
69
|
def inches(self) -> float:
|
|
50
70
|
"""Return value in inches."""
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-pptx"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.76"
|
|
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
|