athena-python-pptx 0.8.1__tar.gz → 0.9.0__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.8.1 → athena_python_pptx-0.9.0}/CHANGELOG.md +27 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/PKG-INFO +1 -1
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/docs/API_PARITY_EXCEPTIONS.md +13 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/batching.py +11 -2
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/client.py +5 -3
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/commands.py +38 -1
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/slides.py +119 -3
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/text/__init__.py +28 -3
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/typing.py +2 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pyproject.toml +1 -1
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/.gitignore +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/README.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/_citations.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/_references.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/__init__.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/units.py +0 -0
- {athena_python_pptx-0.8.1 → athena_python_pptx-0.9.0}/pptx/util.py +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.9.0 — eager validation, table overflow diagnostics, and text caps
|
|
6
|
+
|
|
7
|
+
Publishes the SDK changes already deployed with PPTX Studio after 0.8.1.
|
|
8
|
+
|
|
9
|
+
- Invalid buffered commands now raise at the originating SDK call instead of
|
|
10
|
+
waiting until `flush()`. Validation errors include command context such as
|
|
11
|
+
the slide index, shape ID, or name, making the offending edit actionable
|
|
12
|
+
without sacrificing the final send-time validation backstop.
|
|
13
|
+
- `Slide.validate()` now warns when table-cell text is likely to wrap beyond
|
|
14
|
+
the available column width. The offline heuristic accounts for cell margins,
|
|
15
|
+
font size, merged-column spans, digits, and common punctuation.
|
|
16
|
+
- `Font.cap` and the `TextCap` type expose PowerPoint's native `none`, `all`,
|
|
17
|
+
and `small` capitalization modes. The value hydrates from existing decks and
|
|
18
|
+
survives full-frame text re-emission.
|
|
19
|
+
|
|
20
|
+
The public text-capitalization surface makes this a minor release.
|
|
21
|
+
|
|
22
|
+
## 0.8.1 — version-string fix (re-cut of 0.8.0)
|
|
23
|
+
|
|
24
|
+
Functionally identical to 0.8.0. The 0.8.0 wheel was auto-published from a
|
|
25
|
+
tree where `pptx/__init__.py::__version__` was never bumped off `0.7.1`, so
|
|
26
|
+
`import pptx; pptx.__version__` reported `0.7.1` even though the wheel
|
|
27
|
+
contained the surgical-`SetRunText` + faithful-hydration code. PyPI versions
|
|
28
|
+
are immutable, so 0.8.1 re-cuts the release with the corrected `__version__`
|
|
29
|
+
string. No behavior change vs 0.8.0; use 0.8.1 (the `presentation-exec:v55`
|
|
30
|
+
snapshot bundles it).
|
|
31
|
+
|
|
5
32
|
## 0.8.0 — Surgical run text + faithful style hydration
|
|
6
33
|
|
|
7
34
|
Closes the "agent edited deck text and the styling vanished" class of bugs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -164,6 +164,19 @@ takes the rest of the queued edits down with it.
|
|
|
164
164
|
|
|
165
165
|
These properties/methods were added because LLM agents (Claude, GPT, etc.) frequently generate code using these patterns. Without them, agent-generated table code fails at runtime.
|
|
166
166
|
|
|
167
|
+
### `Font.cap` — PowerPoint-native capitalization mode
|
|
168
|
+
|
|
169
|
+
The SDK exposes PowerPoint's OOXML run capitalization property as
|
|
170
|
+
`run.font.cap`, accepting `"none"`, `"all"`, or `"small"`. Stock
|
|
171
|
+
python-pptx 1.0.2 does not expose `a:rPr/@cap`, but the Athena SDK needs a
|
|
172
|
+
typed surface so template-authored all-caps and small-caps text survives
|
|
173
|
+
agent edits and full-frame text re-emission.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
run.font.cap = "all"
|
|
177
|
+
assert run.font.cap == "all"
|
|
178
|
+
```
|
|
179
|
+
|
|
167
180
|
### `TableCell` — RGB tuple aliases
|
|
168
181
|
|
|
169
182
|
| Property | Type | Maps to | Why |
|
|
@@ -19,7 +19,7 @@ from threading import local
|
|
|
19
19
|
from typing import TYPE_CHECKING, Any, Generator, Optional
|
|
20
20
|
|
|
21
21
|
from . import _ptc
|
|
22
|
-
from .commands import AnyCommand
|
|
22
|
+
from .commands import AnyCommand, validate_command
|
|
23
23
|
from .errors import BatchPartialError
|
|
24
24
|
from .typing import CommandsResponse
|
|
25
25
|
|
|
@@ -234,12 +234,21 @@ class CommandBuffer:
|
|
|
234
234
|
|
|
235
235
|
Returns:
|
|
236
236
|
Response if sent immediately, None if buffered
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValidationError: If the command's parameters are invalid. Raised
|
|
240
|
+
here — at the API call that created the command — rather than
|
|
241
|
+
at the eventual ``flush()``, so the traceback points at the
|
|
242
|
+
offending user line and the invalid command is never buffered
|
|
243
|
+
(a flush-time failure would discard the whole pending batch).
|
|
237
244
|
"""
|
|
238
245
|
# Surface any error captured by the background auto-flush timer
|
|
239
246
|
# before accepting more work — otherwise the user could continue
|
|
240
247
|
# queueing commands on top of a partially-failed batch.
|
|
241
248
|
self._raise_deferred_error()
|
|
242
249
|
|
|
250
|
+
validate_command(command)
|
|
251
|
+
|
|
243
252
|
# PTC begin: emit only for commands in the allow-list. Other
|
|
244
253
|
# commands still flow through the buffer (and through the batched
|
|
245
254
|
# HTTP POST), they just don't produce a sub-tool-card. ``emit_end``
|
|
@@ -365,7 +374,7 @@ class CommandBuffer:
|
|
|
365
374
|
if len(chunks) > 1:
|
|
366
375
|
try:
|
|
367
376
|
for command in commands:
|
|
368
|
-
command
|
|
377
|
+
validate_command(command)
|
|
369
378
|
except Exception:
|
|
370
379
|
with self._commands_lock:
|
|
371
380
|
self._commands = commands + self._commands
|
|
@@ -37,7 +37,7 @@ from .typing import (
|
|
|
37
37
|
ExportStatus,
|
|
38
38
|
RenderStatus,
|
|
39
39
|
)
|
|
40
|
-
from .commands import AnyCommand
|
|
40
|
+
from .commands import AnyCommand, validate_command
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
SDK_VERSION = "0.1.0"
|
|
@@ -647,9 +647,11 @@ class Client:
|
|
|
647
647
|
Returns:
|
|
648
648
|
Response with applied status, created IDs, and optional snapshot
|
|
649
649
|
"""
|
|
650
|
-
# Validate all commands
|
|
650
|
+
# Validate all commands (backstop — CommandBuffer.add validates
|
|
651
|
+
# eagerly at the creating call site; this covers direct client use
|
|
652
|
+
# and commands mutated after they were buffered).
|
|
651
653
|
for cmd in commands:
|
|
652
|
-
cmd
|
|
654
|
+
validate_command(cmd)
|
|
653
655
|
|
|
654
656
|
payload = {
|
|
655
657
|
"client": {"type": SDK_CLIENT_TYPE, "version": SDK_VERSION},
|
|
@@ -14,7 +14,7 @@ __athena_extension_description__: str = (
|
|
|
14
14
|
__athena_extension_since__: str = "0.1.0"
|
|
15
15
|
|
|
16
16
|
from dataclasses import dataclass, field, asdict
|
|
17
|
-
from typing import Any, Optional, Union
|
|
17
|
+
from typing import Any, ClassVar, Optional, Union
|
|
18
18
|
from ._citations import normalize_citation_string
|
|
19
19
|
from .errors import ValidationError
|
|
20
20
|
from .typing import ShapeId, SlideId, TextRunPath, TextStyle
|
|
@@ -39,6 +39,26 @@ class Command:
|
|
|
39
39
|
components = snake_str.split("_")
|
|
40
40
|
return components[0] + "".join(x.title() for x in components[1:])
|
|
41
41
|
|
|
42
|
+
# Identifying fields surfaced in validation errors so a failure names the
|
|
43
|
+
# exact command instance ("AddShape(slide_index=4, name='Card 2')") rather
|
|
44
|
+
# than only the bad value.
|
|
45
|
+
_CONTEXT_FIELDS: ClassVar[tuple[str, ...]] = (
|
|
46
|
+
"slide_index",
|
|
47
|
+
"index",
|
|
48
|
+
"shape_id",
|
|
49
|
+
"name",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def context_label(self) -> str:
|
|
53
|
+
"""Short human-readable identity for this command instance."""
|
|
54
|
+
parts: list[str] = []
|
|
55
|
+
for field_name in self._CONTEXT_FIELDS:
|
|
56
|
+
value = getattr(self, field_name, None)
|
|
57
|
+
if value is not None:
|
|
58
|
+
parts.append(f"{field_name}={value!r}")
|
|
59
|
+
label = type(self).__name__
|
|
60
|
+
return f"{label}({', '.join(parts)})" if parts else label
|
|
61
|
+
|
|
42
62
|
def validate(self) -> None:
|
|
43
63
|
"""Validate command parameters. Raises ValidationError if invalid."""
|
|
44
64
|
pass
|
|
@@ -49,6 +69,23 @@ class Command:
|
|
|
49
69
|
raise NotImplementedError
|
|
50
70
|
|
|
51
71
|
|
|
72
|
+
def validate_command(command: "Command") -> None:
|
|
73
|
+
"""Validate ``command``, prefixing any ValidationError with its identity.
|
|
74
|
+
|
|
75
|
+
The bare per-command ``validate()`` messages name only the bad field/value
|
|
76
|
+
("height must be >= 0 EMU"); when validation runs away from the creating
|
|
77
|
+
call site (batched flush, multi-command POST) that alone can't be traced
|
|
78
|
+
back to a shape or slide. Every SDK-internal validation path goes through
|
|
79
|
+
this wrapper so the error always carries the command context.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
command.validate()
|
|
83
|
+
except ValidationError as exc:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
f"{command.context_label()}: {exc.message}", exc.field
|
|
86
|
+
) from None
|
|
87
|
+
|
|
88
|
+
|
|
52
89
|
@dataclass
|
|
53
90
|
class AddTextBox(Command):
|
|
54
91
|
"""
|
|
@@ -41,6 +41,106 @@ def _normalize_layout_name(name: str) -> str:
|
|
|
41
41
|
return " ".join(name.strip().lower().replace("_", " ").split())
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# --- table text-overflow heuristic (used by Slide.validate) ----------------
|
|
45
|
+
# Approximate per-glyph advance widths in em units (Arial-ish). Deliberately
|
|
46
|
+
# conservative and digit/punctuation-aware because the common overflow case is
|
|
47
|
+
# wide numeric values (currency / counts) in narrow columns.
|
|
48
|
+
_TABLE_GLYPH_EM = {
|
|
49
|
+
**{d: 0.556 for d in "0123456789"},
|
|
50
|
+
",": 0.278, ".": 0.278, "(": 0.333, ")": 0.333,
|
|
51
|
+
"%": 0.889, "$": 0.556, "-": 0.333, " ": 0.278, "/": 0.278,
|
|
52
|
+
}
|
|
53
|
+
_EMU_PER_PT = 12700
|
|
54
|
+
# PowerPoint's default table body font when a cell exposes no explicit size.
|
|
55
|
+
_DEFAULT_TABLE_FONT_PT = 18.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _estimate_text_width_emu(text: str, font_pt: float) -> float:
|
|
59
|
+
"""Conservative single-line width estimate for ``text`` at ``font_pt``."""
|
|
60
|
+
return sum(_TABLE_GLYPH_EM.get(ch, 0.5) for ch in text) * font_pt * _EMU_PER_PT
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cell_font_pt(cell: Any) -> Optional[float]:
|
|
64
|
+
"""Resolve a cell's font size (pt): cell-level, else first run, else None."""
|
|
65
|
+
fp = getattr(cell, "font_size_pt", None)
|
|
66
|
+
if fp:
|
|
67
|
+
return float(fp)
|
|
68
|
+
try:
|
|
69
|
+
for para in cell.text_frame.paragraphs:
|
|
70
|
+
for run in para.runs:
|
|
71
|
+
if run.font is not None and run.font.size is not None:
|
|
72
|
+
# SDK run font size is a raw EMU int (no .pt); Length
|
|
73
|
+
# objects do expose .pt — handle both.
|
|
74
|
+
size = run.font.size
|
|
75
|
+
return float(getattr(size, "pt", size / _EMU_PER_PT))
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _table_overflow_warnings(table: Any, label: str) -> list[str]:
|
|
82
|
+
"""Heuristic warnings for table cells whose single-line text likely
|
|
83
|
+
exceeds the column width (minus cell margins) and would wrap — growing the
|
|
84
|
+
row and breaking alignment — in PowerPoint. No rendering; uses a
|
|
85
|
+
conservative font-metric estimate.
|
|
86
|
+
|
|
87
|
+
Skips columns whose width is unknown (e.g. tables cloned server-side whose
|
|
88
|
+
widths are not in the local model) and resolves the font size per cell
|
|
89
|
+
(cell-level -> first run -> the PowerPoint default). Being a heuristic it
|
|
90
|
+
can occasionally false-positive on intentionally tight cells.
|
|
91
|
+
"""
|
|
92
|
+
out: list[str] = []
|
|
93
|
+
try:
|
|
94
|
+
ncols = int(table.cols)
|
|
95
|
+
nrows = len(table.rows)
|
|
96
|
+
except Exception:
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
def _colw(i: int) -> int:
|
|
100
|
+
try:
|
|
101
|
+
w = table.columns[i].width
|
|
102
|
+
return int(w) if w and int(w) > 0 else 0
|
|
103
|
+
except Exception:
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
for r in range(nrows):
|
|
107
|
+
for c in range(ncols):
|
|
108
|
+
try:
|
|
109
|
+
cell = table.cell(r, c)
|
|
110
|
+
except Exception:
|
|
111
|
+
continue
|
|
112
|
+
# Continuation cells of a horizontal merge hold no text of their
|
|
113
|
+
# own — their width belongs to the merge origin.
|
|
114
|
+
if getattr(cell, "_h_merge", False):
|
|
115
|
+
continue
|
|
116
|
+
text = (cell.text or "").strip()
|
|
117
|
+
if not text:
|
|
118
|
+
continue
|
|
119
|
+
# A horizontally-merged origin cell spans grid_span columns; its
|
|
120
|
+
# usable width is the sum of those columns (PowerPoint semantics).
|
|
121
|
+
span = getattr(cell, "grid_span", None) or getattr(cell, "_grid_span", 1) or 1
|
|
122
|
+
colw = sum(_colw(c + k) for k in range(int(span)))
|
|
123
|
+
if colw <= 0:
|
|
124
|
+
continue # unknown/zero width — cannot judge overflow
|
|
125
|
+
font_pt = _cell_font_pt(cell) or _DEFAULT_TABLE_FONT_PT
|
|
126
|
+
try:
|
|
127
|
+
avail = colw - int(cell.margin_left) - int(cell.margin_right)
|
|
128
|
+
except Exception:
|
|
129
|
+
avail = colw
|
|
130
|
+
if avail <= 0:
|
|
131
|
+
continue
|
|
132
|
+
est = _estimate_text_width_emu(text, font_pt)
|
|
133
|
+
if est > avail:
|
|
134
|
+
span_note = f" across {int(span)} cols" if int(span) > 1 else ""
|
|
135
|
+
out.append(
|
|
136
|
+
f"table '{label}' cell r{r}c{c} text {text!r} "
|
|
137
|
+
f"(~{int(est)} EMU at {font_pt:g}pt) likely overflows column "
|
|
138
|
+
f"width {colw} EMU{span_note} (~{avail} EMU after cell "
|
|
139
|
+
f"margins) — will wrap and grow the row in PowerPoint"
|
|
140
|
+
)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
44
144
|
def _visible_element_ids(
|
|
45
145
|
*,
|
|
46
146
|
slide_snapshot: SlideSnapshot,
|
|
@@ -1158,10 +1258,16 @@ class Slide:
|
|
|
1158
1258
|
can ignore those warnings; the goal is to catch the *accidental*
|
|
1159
1259
|
on-top-of-each-other case (the "235 shapes layered" failure
|
|
1160
1260
|
from the agent retrospective).
|
|
1261
|
+
* **Table cell text overflow** — for shapes that carry a table, cells
|
|
1262
|
+
whose single-line text is likely wider than the column (minus cell
|
|
1263
|
+
margins) and would wrap — growing the row and breaking alignment —
|
|
1264
|
+
when opened in PowerPoint. Uses a conservative font-metric estimate
|
|
1265
|
+
(no rendering), so it catches the "looks fine in the Studio preview
|
|
1266
|
+
but wraps in PowerPoint" class for tables. Heuristic: may
|
|
1267
|
+
occasionally false-positive; skips columns of unknown width.
|
|
1161
1268
|
|
|
1162
|
-
Limitations:
|
|
1163
|
-
|
|
1164
|
-
inspect the PNG.
|
|
1269
|
+
Limitations: for *non-table* text it does not render and so cannot
|
|
1270
|
+
detect overflow — for that, render the slide and inspect the PNG.
|
|
1165
1271
|
"""
|
|
1166
1272
|
prs = self._presentation
|
|
1167
1273
|
slide_w = int(prs.slide_width) if prs.slide_width else 12192000
|
|
@@ -1200,6 +1306,16 @@ class Slide:
|
|
|
1200
1306
|
f"({overlap_area} EMU² ≈ "
|
|
1201
1307
|
f"{overlap_area / (slide_w * slide_h) * 100:.2f}% of slide)"
|
|
1202
1308
|
)
|
|
1309
|
+
|
|
1310
|
+
# Table cell text-overflow (heuristic; no render). Catches the
|
|
1311
|
+
# "looks fine in Studio, wraps in PowerPoint" class for tables.
|
|
1312
|
+
for shape in list(self._shapes):
|
|
1313
|
+
if getattr(shape, "has_table", False):
|
|
1314
|
+
try:
|
|
1315
|
+
label = shape.name or shape.shape_id
|
|
1316
|
+
warnings.extend(_table_overflow_warnings(shape.table, label))
|
|
1317
|
+
except Exception:
|
|
1318
|
+
pass
|
|
1203
1319
|
return warnings
|
|
1204
1320
|
|
|
1205
1321
|
@athena_only(
|
|
@@ -16,7 +16,7 @@ from ..commands import (
|
|
|
16
16
|
)
|
|
17
17
|
from ..dml.color import RGBColor, ColorFormat, scheme_name_to_theme_color, theme_color_to_scheme_name
|
|
18
18
|
from ..errors import UnsupportedFeatureError
|
|
19
|
-
from ..typing import ShapeId, TextStyle, TextFramePropertiesSnapshot
|
|
19
|
+
from ..typing import ShapeId, TextCap, TextStyle, TextFramePropertiesSnapshot
|
|
20
20
|
from .._athena_extension import athena_extension
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
@@ -36,7 +36,7 @@ class PP_ALIGN:
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# Wire style keys the SDK hydrates into typed Font state. Anything else the
|
|
39
|
-
# server sends (``
|
|
39
|
+
# server sends (``highlightColorHex``, ``highlightSchemeColor``,
|
|
40
40
|
# future keys) is carried verbatim in ``Font._extra_style`` so full-frame
|
|
41
41
|
# re-emits (run.text / paragraph.text / add_paragraph) don't silently strip
|
|
42
42
|
# properties the SDK doesn't model.
|
|
@@ -44,6 +44,7 @@ _TYPED_WIRE_STYLE_KEYS = frozenset({
|
|
|
44
44
|
"bold", "italic", "underline", "fontSizePt", "fontFamily", "fontRef",
|
|
45
45
|
"colorHex", "spacingPt", "strike", "strikethrough", "baseline",
|
|
46
46
|
"subscript", "superscript", "languageId", "schemeColor", "brightness",
|
|
47
|
+
"cap",
|
|
47
48
|
})
|
|
48
49
|
|
|
49
50
|
|
|
@@ -212,6 +213,7 @@ class Font:
|
|
|
212
213
|
strike: Optional[bool] = None,
|
|
213
214
|
baseline: Optional[int] = None,
|
|
214
215
|
language_id: Optional[int] = None,
|
|
216
|
+
cap: Optional[TextCap] = None,
|
|
215
217
|
change_listener: Optional[Any] = None,
|
|
216
218
|
):
|
|
217
219
|
self._run = run
|
|
@@ -254,8 +256,9 @@ class Font:
|
|
|
254
256
|
self._language_id: Optional[int] = (
|
|
255
257
|
int(language_id) if language_id is not None else None
|
|
256
258
|
)
|
|
259
|
+
self._cap = cap
|
|
257
260
|
# Wire style properties hydrated from the server that the SDK doesn't
|
|
258
|
-
# model as typed fields (
|
|
261
|
+
# model as typed fields (highlights, …). Carried verbatim so
|
|
259
262
|
# ``_build_style_dict`` reproduces them on full-frame re-emits instead
|
|
260
263
|
# of stripping them from the deck.
|
|
261
264
|
self._extra_style: dict[str, Any] = {}
|
|
@@ -293,6 +296,23 @@ class Font:
|
|
|
293
296
|
self._underline = value
|
|
294
297
|
self._emit_style_change()
|
|
295
298
|
|
|
299
|
+
@property
|
|
300
|
+
def cap(self) -> Optional[TextCap]:
|
|
301
|
+
"""Text capitalization mode (``none``, ``all``, or ``small``).
|
|
302
|
+
|
|
303
|
+
This Athena extension maps directly to OOXML ``a:rPr/@cap``. Use
|
|
304
|
+
``"all"`` for template titles whose uppercase appearance is authored
|
|
305
|
+
as a run property rather than baked into the text value.
|
|
306
|
+
"""
|
|
307
|
+
return self._cap
|
|
308
|
+
|
|
309
|
+
@cap.setter
|
|
310
|
+
def cap(self, value: TextCap) -> None:
|
|
311
|
+
if value not in {"none", "all", "small"}:
|
|
312
|
+
raise ValueError("cap must be one of: 'none', 'all', 'small'")
|
|
313
|
+
self._cap = value
|
|
314
|
+
self._emit_style_change()
|
|
315
|
+
|
|
296
316
|
@property
|
|
297
317
|
def size(self) -> Optional[int]:
|
|
298
318
|
"""Font size in EMU."""
|
|
@@ -533,6 +553,8 @@ class Font:
|
|
|
533
553
|
style["baseline"] = self._baseline
|
|
534
554
|
if self._language_id is not None:
|
|
535
555
|
style["languageId"] = self._language_id
|
|
556
|
+
if self._cap is not None:
|
|
557
|
+
style["cap"] = self._cap
|
|
536
558
|
return style
|
|
537
559
|
|
|
538
560
|
def _style_payload(self) -> "TextStyle":
|
|
@@ -605,6 +627,8 @@ class Font:
|
|
|
605
627
|
style["baseline"] = self._baseline
|
|
606
628
|
if self._language_id is not None:
|
|
607
629
|
style["languageId"] = self._language_id
|
|
630
|
+
if self._cap is not None:
|
|
631
|
+
style["cap"] = self._cap
|
|
608
632
|
|
|
609
633
|
@athena_extension(
|
|
610
634
|
issue=1063,
|
|
@@ -959,6 +983,7 @@ class Run:
|
|
|
959
983
|
strike=strike,
|
|
960
984
|
baseline=_baseline_from_wire(style_dict),
|
|
961
985
|
language_id=style_dict.get("languageId"),
|
|
986
|
+
cap=style_dict.get("cap"),
|
|
962
987
|
)
|
|
963
988
|
self._font._hydrate_theme_color(
|
|
964
989
|
style_dict.get("schemeColor"), style_dict.get("brightness")
|
|
@@ -16,6 +16,7 @@ from enum import IntEnum
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# Type aliases for IDs
|
|
19
|
+
TextCap = Literal["none", "all", "small"]
|
|
19
20
|
DeckId = str
|
|
20
21
|
SlideId = str
|
|
21
22
|
ShapeId = str
|
|
@@ -153,6 +154,7 @@ class TextStyle(TypedDict, total=False):
|
|
|
153
154
|
strike: bool
|
|
154
155
|
baseline: int
|
|
155
156
|
languageId: int
|
|
157
|
+
cap: TextCap
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
@dataclass
|
|
@@ -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.9.0"
|
|
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
|