Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/presentation_tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -5080,4 +5080,4 @@
}
]
}
}
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tppt"
version = "0.2.2"
version = "0.3.0"
description = "Typed Python PowerPoint Tool"
readme = "README.md"
requires-python = ">=3.11.0"
Expand Down
1 change: 1 addition & 0 deletions src/tppt/pptx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
from .shape.text import Text as Text
from .slide import Slide as Slide
from .table import Table as Table
from .table import TableBorderStyle as TableBorderStyle
from .table import TableCellStyle as TableCellStyle
22 changes: 15 additions & 7 deletions src/tppt/pptx/chart/chart.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from typing import TYPE_CHECKING, Literal, Self, TypedDict, assert_never

from pptx.chart.chart import Chart as PptxChart
from pptx.chart.chart import Legend as PptxLegend
from pptx.chart.chart import ChartTitle as PptxChartTitle
from pptx.chart.data import BubbleChartData as PptxBubbleChartData
from pptx.chart.data import CategoryChartData as PptxCategoryChartData
from pptx.chart.data import ChartData as PptxChartData
from pptx.chart.data import XyChartData as PptxXyChartData
from pptx.chart.legend import Legend as PptxLegend
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION

from tppt.pptx.converter import PptxConvertible
from tppt.types._length import Length, LiteralLength

if TYPE_CHECKING:
from pptx.chart.chart import ChartTitle as PptxChartTitle

from tppt.pptx.text.font import Font
from tppt.pptx.text.text_frame import TextFrame

PptxChartDataLike = (
PptxChartData | PptxCategoryChartData | PptxXyChartData | PptxBubbleChartData
)

LiteralChartType = Literal[
"3D Area",
"3D Stacked Area",
Expand Down Expand Up @@ -253,7 +259,7 @@ class ChartProps(TypedDict):
y: Length | LiteralLength
cx: Length | LiteralLength
cy: Length | LiteralLength
chart_data: PptxChartData
chart_data: PptxChartDataLike


class ChartData(ChartProps):
Expand All @@ -262,7 +268,7 @@ class ChartData(ChartProps):
type: Literal["chart"]


class ChartTitle(PptxConvertible["PptxChartTitle"]):
class ChartTitle(PptxConvertible[PptxChartTitle]):
"""Chart title wrapper class."""

@property
Expand Down Expand Up @@ -399,8 +405,10 @@ def set_has_title(self, value: bool) -> Self:
@property
def legend(self) -> Legend:
"""Legend of the chart."""
return Legend(self._pptx.legend)
legend = self._pptx.legend
assert legend is not None
return Legend(legend)

def replace_data(self, chart_data: PptxChartData) -> None:
def replace_data(self, chart_data: PptxChartDataLike) -> None:
"""Replace chart data with new data."""
self._pptx.replace_data(chart_data)
13 changes: 6 additions & 7 deletions src/tppt/pptx/dml/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,14 @@ def set_brightness(self, value: float) -> Self:

@property
def rgb(self) -> Color:
solid_fill = cast(
_Element, self._pptx._xFill.solidFill.get_or_change_to_srgbClr()
srgb_clr = cast(_Element, cast(_SRgbColor, self._pptx._color)._srgbClr)
alpha_elem = srgb_clr.find("a:alpha", namespace)
alpha = (
round(int(alpha_elem.attrib["val"]) * 255 / 100000)
if alpha_elem is not None
else None
)

if alpha := solid_fill.find("a:alpha", namespace):
alpha = alpha.attrib["val"]
else:
alpha = None

return to_tppt_rgb_color(cast(PptxRGBColor, self._pptx.rgb), alpha=alpha)

@rgb.setter
Expand Down
2 changes: 1 addition & 1 deletion src/tppt/pptx/dml/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def width(self) -> EnglishMetricUnits | None:

@width.setter
def width(self, value: EnglishMetricUnits | _PptxEmu) -> None:
self._pptx.width = to_english_metric_units(value)
self._pptx.width = to_english_metric_units(value).value

def set_width(self, value: EnglishMetricUnits | _PptxEmu) -> "LineFormat":
self.width = value
Expand Down
14 changes: 9 additions & 5 deletions src/tppt/pptx/notes_slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

if TYPE_CHECKING:
from tppt.pptx.shape import BaseShape
from tppt.pptx.shape.placeholder import SlidePlaceholder
from tppt.pptx.shape.placeholder import NotesSlidePlaceholder, SlidePlaceholder
from tppt.pptx.text.text_frame import TextFrame


Expand All @@ -18,14 +18,18 @@ def notes_text_frame(self) -> "TextFrame":
"""Text frame of the notes body placeholder."""
from tppt.pptx.text.text_frame import TextFrame

return TextFrame(self._pptx.notes_text_frame)
text_frame = self._pptx.notes_text_frame
assert text_frame is not None
return TextFrame(text_frame)

@property
def notes_placeholder(self) -> "SlidePlaceholder":
def notes_placeholder(self) -> "NotesSlidePlaceholder":
"""Notes body placeholder shape."""
from tppt.pptx.shape.placeholder import SlidePlaceholder
from tppt.pptx.shape.placeholder import NotesSlidePlaceholder

return SlidePlaceholder(self._pptx.notes_placeholder)
placeholder = self._pptx.notes_placeholder
assert placeholder is not None
return NotesSlidePlaceholder(placeholder)

@property
def placeholders(self) -> "list[SlidePlaceholder]":
Expand Down
17 changes: 13 additions & 4 deletions src/tppt/pptx/shape/picture.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
from typing import IO, TYPE_CHECKING, Literal, NotRequired, Self, TypedDict, assert_never
from typing import (
IO,
TYPE_CHECKING,
Literal,
NotRequired,
Self,
TypedDict,
assert_never,
cast,
)

from pptx.opc.constants import CONTENT_TYPE
from pptx.shapes.picture import Movie as PptxMovie
Expand All @@ -10,7 +19,7 @@

if TYPE_CHECKING:
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
from pptx.image import Image
from pptx.parts.image import Image

from tppt.pptx.dml.line import LineFormat

Expand Down Expand Up @@ -185,11 +194,11 @@ def __init__(
self._pptx = pptx_obj

@property
def poster_frame(self) -> "Image":
def poster_frame(self) -> "Image | None":
"""Poster frame image for the movie."""
return self._pptx.poster_frame

@property
def media_type(self) -> str | None:
"""Media type (MIME type) of the movie."""
return self._pptx.media_type
return cast(str | None, self._pptx.media_type)
4 changes: 4 additions & 0 deletions src/tppt/pptx/shape/placeholder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pptx.shapes.placeholder import LayoutPlaceholder as _PptxLayoutPlaceholder
from pptx.shapes.placeholder import MasterPlaceholder as _PptxMasterPlaceholder
from pptx.shapes.placeholder import NotesSlidePlaceholder as _PptxNotesSlidePlaceholder
from pptx.shapes.placeholder import SlidePlaceholder as _PptxSlidePlaceholder

from . import Shape
Expand All @@ -11,4 +12,7 @@ class LayoutPlaceholder(Shape[_PptxLayoutPlaceholder]): ...
class MasterPlaceholder(Shape[_PptxMasterPlaceholder]): ...


class NotesSlidePlaceholder(Shape[_PptxNotesSlidePlaceholder]): ...


class SlidePlaceholder(Shape[_PptxSlidePlaceholder]): ...
3 changes: 3 additions & 0 deletions src/tppt/pptx/shape/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class TextProps(RangeProps):
bold: NotRequired[bool]
italic: NotRequired[bool]
color: NotRequired[Color | LiteralColor]
font_name: NotRequired[str]
margin_bottom: NotRequired[Length | LiteralLength]
margin_left: NotRequired[Length | LiteralLength]
vertical_anchor: NotRequired[MSO_ANCHOR]
Expand Down Expand Up @@ -52,6 +53,8 @@ def __init__(self, pptx_obj: PptxShape, data: TextData | None = None, /) -> None
font.italic = italic
if (color := data.get("color")) is not None:
font.color.rgb = to_color(color)
if (font_name := data.get("font_name")) is not None:
font.name = font_name
if (margin_bottom := data.get("margin_bottom")) is not None:
p.space_after = to_length(margin_bottom)
if (margin_left := data.get("margin_left")) is not None:
Expand Down
86 changes: 81 additions & 5 deletions src/tppt/pptx/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
overload,
)

from pptx.chart.data import ChartData as PptxChartData
from pptx.slide import Slide as PptxSlide
from pptx.slide import _BaseSlide as _PptxBaseSlide

Expand All @@ -24,9 +25,9 @@
PptxMovie,
to_pptx_movie_mime_type,
)
from tppt.types import FilePath
from tppt.types import Color, FilePath, Length, LiteralColor, LiteralLength

from .converter import PptxConvertible, to_pptx_length
from .converter import PptxConvertible, to_pptx_length, to_pptx_rgb_color
from .shape import BaseShape, RangeProps, Shape
from .shape.picture import Picture, PictureData, PictureProps
from .shape.placeholder import SlidePlaceholder
Expand Down Expand Up @@ -111,6 +112,15 @@ def slide_layout(self) -> SlideLayout:
class SlideBuilder:
"""Slide builder."""

class AddShapeProps(RangeProps, total=False):
fill_color: Color | LiteralColor
fill_alpha: float
line_color: Color | LiteralColor
line_width: Length | LiteralLength
text: str
font_size: Length | LiteralLength
font_color: Color | LiteralColor

def __init__(
self,
slide_layout: SlideLayout,
Expand Down Expand Up @@ -349,7 +359,7 @@ def _register(slide: Slide) -> Chart:
y=to_pptx_length(data["y"]),
cx=to_pptx_length(data["cx"]),
cy=to_pptx_length(data["cy"]),
chart_data=data["chart_data"],
chart_data=cast(PptxChartData, data["chart_data"]),
)
)
if isinstance(data, Callable):
Expand All @@ -366,7 +376,7 @@ def add_shape(
self,
shape_type: "MSO_AUTO_SHAPE_TYPE",
/,
**kwargs: Unpack[RangeProps],
**kwargs: Unpack[AddShapeProps],
) -> Self: ...

@overload
Expand All @@ -375,14 +385,21 @@ def add_shape(
shape_type: "MSO_AUTO_SHAPE_TYPE",
shape: Callable[[Shape], Shape],
/,
**kwargs: Unpack[RangeProps],
**kwargs: Unpack[AddShapeProps],
) -> Self: ...

def add_shape(
self,
shape_type: "MSO_AUTO_SHAPE_TYPE",
shape: Callable[[Shape], Shape] | None = None,
/,
fill_color: "Color | LiteralColor | None" = None,
fill_alpha: float | None = None,
line_color: "Color | LiteralColor | None" = None,
line_width: "Length | LiteralLength | None" = None,
text: str | None = None,
font_size: "Length | LiteralLength | None" = None,
font_color: "Color | LiteralColor | None" = None,
**kwargs: Unpack[RangeProps],
) -> Self:
"""Add a shape to the slide."""
Expand All @@ -397,6 +414,56 @@ def _register(slide: Slide) -> Shape:
to_pptx_length(kwargs["height"]),
)
)

if fill_color is not None:
rgb, alpha = to_pptx_rgb_color(fill_color)
shape_obj.fill.solid().fore_color.set_rgb(fill_color)

if fill_alpha is not None:
from lxml.etree import _Element

from pptx.oxml.ns import _nsmap as namespace
from pptx.oxml.xmlchemy import OxmlElement

pptx_shape = shape_obj.to_pptx()
solid_fill = cast(
_Element,
pptx_shape.fill._xPr.solidFill, # type: ignore[union-attr]
)
# Remove existing alpha element if any
existing_alpha = solid_fill.find("a:alpha", namespace)
if existing_alpha is not None:
solid_fill.remove(existing_alpha)
alpha_elem = OxmlElement("a:alpha")
alpha_elem.attrib["val"] = str(int(fill_alpha * 100000))
# Find srgbClr or other color child to append alpha to
for child in solid_fill:
child.append(alpha_elem)
break

if line_color is not None:
shape_obj.line.fill.solid().fore_color.set_rgb(line_color)

if line_width is not None:
from tppt.types._length import EnglishMetricUnits

pptx_len = to_pptx_length(line_width)
shape_obj.line.set_width(EnglishMetricUnits(int(pptx_len)))

if text is not None:
shape_obj.text = text

if font_size is not None or font_color is not None:
tf = shape_obj.to_pptx().text_frame
if tf.paragraphs:
paragraph = tf.paragraphs[0]
run = paragraph.runs[0] if paragraph.runs else paragraph.add_run()
if font_size is not None:
run.font.size = to_pptx_length(font_size)
if font_color is not None:
rgb_val, _ = to_pptx_rgb_color(font_color)
run.font.color.rgb = rgb_val

if shape is not None:
return shape(shape_obj)
else:
Expand All @@ -405,6 +472,15 @@ def _register(slide: Slide) -> Shape:
self._shape_registry.append(_register)
return self

def customize(self, callback: Callable[[PptxSlide], None]) -> Self:
"""Register a callback for direct access to the python-pptx Slide object."""

def _register(slide: Slide) -> None:
callback(slide.to_pptx())

self._shape_registry.append(_register)
return self

def tap(self, callback: Callable[[Slide], None]) -> Self:
"""Register a callback for direct slide access."""
self._shape_registry.append(callback)
Expand Down
1 change: 1 addition & 0 deletions src/tppt/pptx/table/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
from .table import Row as Row
from .table import RowCollection as RowCollection
from .table import Table as Table
from .table import TableBorderStyle as TableBorderStyle
from .table import TableCellStyle as TableCellStyle
Loading