init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
@@ -0,0 +1,19 @@
"""Base shape-related objects such as BaseShape."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from pptx.oxml.shapes.autoshape import CT_Shape
from pptx.oxml.shapes.connector import CT_Connector
from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
from pptx.oxml.shapes.groupshape import CT_GroupShape
from pptx.oxml.shapes.picture import CT_Picture
ShapeElement: TypeAlias = (
"CT_Connector | CT_GraphicalObjectFrame | CT_GroupShape | CT_Picture | CT_Shape"
)
@@ -0,0 +1,455 @@
# pyright: reportPrivateUsage=false
"""lxml custom element classes for shape-related XML elements."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, cast
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.shapes.shared import BaseShapeElement
from pptx.oxml.simpletypes import (
ST_Coordinate,
ST_PositiveCoordinate,
XsdBoolean,
XsdString,
)
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
)
if TYPE_CHECKING:
from pptx.oxml.shapes.shared import (
CT_ApplicationNonVisualDrawingProps,
CT_NonVisualDrawingProps,
CT_ShapeProperties,
)
from pptx.util import Length
class CT_AdjPoint2D(BaseOxmlElement):
"""`a:pt` custom element class."""
x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType]
y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType]
class CT_CustomGeometry2D(BaseOxmlElement):
"""`a:custGeom` custom element class."""
get_or_add_pathLst: Callable[[], CT_Path2DList]
_tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst")
pathLst: CT_Path2DList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:pathLst", successors=_tag_seq[6:]
)
class CT_GeomGuide(BaseOxmlElement):
"""`a:gd` custom element class.
Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape.
"""
name: str = RequiredAttribute("name", XsdString) # pyright: ignore[reportAssignmentType]
fmla: str = RequiredAttribute("fmla", XsdString) # pyright: ignore[reportAssignmentType]
class CT_GeomGuideList(BaseOxmlElement):
"""`a:avLst` custom element class."""
_add_gd: Callable[[], CT_GeomGuide]
gd_lst: list[CT_GeomGuide]
gd = ZeroOrMore("a:gd")
class CT_NonVisualDrawingShapeProps(BaseShapeElement):
"""`p:cNvSpPr` custom element class."""
spLocks = ZeroOrOne("a:spLocks")
txBox: bool | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"txBox", XsdBoolean
)
class CT_Path2D(BaseOxmlElement):
"""`a:path` custom element class."""
_add_close: Callable[[], CT_Path2DClose]
_add_lnTo: Callable[[], CT_Path2DLineTo]
_add_moveTo: Callable[[], CT_Path2DMoveTo]
close = ZeroOrMore("a:close", successors=())
lnTo = ZeroOrMore("a:lnTo", successors=())
moveTo = ZeroOrMore("a:moveTo", successors=())
w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w", ST_PositiveCoordinate
)
h: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"h", ST_PositiveCoordinate
)
def add_close(self) -> CT_Path2DClose:
"""Return a newly created `a:close` element.
The new `a:close` element is appended to this `a:path` element.
"""
return self._add_close()
def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo:
"""Return a newly created `a:lnTo` subtree with end point *(x, y)*.
The new `a:lnTo` element is appended to this `a:path` element.
"""
lnTo = self._add_lnTo()
pt = lnTo._add_pt()
pt.x, pt.y = x, y
return lnTo
def add_moveTo(self, x: Length, y: Length):
"""Return a newly created `a:moveTo` subtree with point `(x, y)`.
The new `a:moveTo` element is appended to this `a:path` element.
"""
moveTo = self._add_moveTo()
pt = moveTo._add_pt()
pt.x, pt.y = x, y
return moveTo
class CT_Path2DClose(BaseOxmlElement):
"""`a:close` custom element class."""
class CT_Path2DLineTo(BaseOxmlElement):
"""`a:lnTo` custom element class."""
_add_pt: Callable[[], CT_AdjPoint2D]
pt = ZeroOrOne("a:pt", successors=())
class CT_Path2DList(BaseOxmlElement):
"""`a:pathLst` custom element class."""
_add_path: Callable[[], CT_Path2D]
path = ZeroOrMore("a:path", successors=())
def add_path(self, w: Length, h: Length):
"""Return a newly created `a:path` child element."""
path = self._add_path()
path.w, path.h = w, h
return path
class CT_Path2DMoveTo(BaseOxmlElement):
"""`a:moveTo` custom element class."""
_add_pt: Callable[[], CT_AdjPoint2D]
pt = ZeroOrOne("a:pt", successors=())
class CT_PresetGeometry2D(BaseOxmlElement):
"""`a:prstGeom` custom element class."""
_add_avLst: Callable[[], CT_GeomGuideList]
_remove_avLst: Callable[[], None]
avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst") # pyright: ignore[reportAssignmentType]
prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"prst", MSO_AUTO_SHAPE_TYPE
)
@property
def gd_lst(self) -> list[CT_GeomGuide]:
"""Sequence of `a:gd` element children of `a:avLst`. Empty if none are present."""
avLst = self.avLst
if avLst is None:
return []
return avLst.gd_lst
def rewrite_guides(self, guides: list[tuple[str, int]]):
"""Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`."""
self._remove_avLst()
avLst = self._add_avLst()
for name, val in guides:
gd = avLst._add_gd()
gd.name = name
gd.fmla = "val %d" % val
class CT_Shape(BaseShapeElement):
"""`p:sp` custom element class."""
get_or_add_txBody: Callable[[], CT_TextBody]
nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr") # pyright: ignore[reportAssignmentType]
spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",)) # pyright: ignore
def add_path(self, w: Length, h: Length) -> CT_Path2D:
custGeom = self.spPr.custGeom
if custGeom is None:
raise ValueError("shape must be freeform")
pathLst = custGeom.get_or_add_pathLst()
return pathLst.add_path(w=w, h=h)
def get_or_add_ln(self):
"""Return the `a:ln` grandchild element, newly added if not present."""
return self.spPr.get_or_add_ln()
@property
def has_custom_geometry(self):
"""True if this shape has custom geometry, i.e. is a freeform shape.
A shape has custom geometry if it has a `p:spPr/a:custGeom`
descendant (instead of `p:spPr/a:prstGeom`).
"""
return self.spPr.custGeom is not None
@property
def is_autoshape(self):
"""True if this shape is an auto shape.
A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1"
attribute on cNvSpPr.
"""
prstGeom = self.prstGeom
if prstGeom is None:
return False
return self.nvSpPr.cNvSpPr.txBox is not True
@property
def is_textbox(self):
"""True if this shape is a text box.
A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|.
The default when the txBox attribute is missing is |False|.
"""
return self.nvSpPr.cNvSpPr.txBox is True
@property
def ln(self):
"""`a:ln` grand-child element or |None| if not present."""
return self.spPr.ln
@staticmethod
def new_autoshape_sp(
id_: int, name: str, prst: str, left: int, top: int, width: int, height: int
) -> CT_Shape:
"""Return a new `p:sp` element tree configured as a base auto shape."""
xml = (
"<p:sp %s>\n"
" <p:nvSpPr>\n"
' <p:cNvPr id="%s" name="%s"/>\n'
" <p:cNvSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvSpPr>\n"
" <p:spPr>\n"
" <a:xfrm>\n"
' <a:off x="%s" y="%s"/>\n'
' <a:ext cx="%s" cy="%s"/>\n'
" </a:xfrm>\n"
' <a:prstGeom prst="%s">\n'
" <a:avLst/>\n"
" </a:prstGeom>\n"
" </p:spPr>\n"
" <p:style>\n"
' <a:lnRef idx="1">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:lnRef>\n"
' <a:fillRef idx="3">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:fillRef>\n"
' <a:effectRef idx="2">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:effectRef>\n"
' <a:fontRef idx="minor">\n'
' <a:schemeClr val="lt1"/>\n'
" </a:fontRef>\n"
" </p:style>\n"
" <p:txBody>\n"
' <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
" <a:lstStyle/>\n"
" <a:p>\n"
' <a:pPr algn="ctr"/>\n'
" </a:p>\n"
" </p:txBody>\n"
"</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s")
) % (id_, name, left, top, width, height, prst)
return cast(CT_Shape, parse_xml(xml))
@staticmethod
def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int):
"""Return new `p:sp` element tree configured as freeform shape.
The returned shape has a `a:custGeom` subtree but no paths in its
path list.
"""
xml = (
"<p:sp %s>\n"
" <p:nvSpPr>\n"
' <p:cNvPr id="%s" name="%s"/>\n'
" <p:cNvSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvSpPr>\n"
" <p:spPr>\n"
" <a:xfrm>\n"
' <a:off x="%s" y="%s"/>\n'
' <a:ext cx="%s" cy="%s"/>\n'
" </a:xfrm>\n"
" <a:custGeom>\n"
" <a:avLst/>\n"
" <a:gdLst/>\n"
" <a:ahLst/>\n"
" <a:cxnLst/>\n"
' <a:rect l="l" t="t" r="r" b="b"/>\n'
" <a:pathLst/>\n"
" </a:custGeom>\n"
" </p:spPr>\n"
" <p:style>\n"
' <a:lnRef idx="1">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:lnRef>\n"
' <a:fillRef idx="3">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:fillRef>\n"
' <a:effectRef idx="2">\n'
' <a:schemeClr val="accent1"/>\n'
" </a:effectRef>\n"
' <a:fontRef idx="minor">\n'
' <a:schemeClr val="lt1"/>\n'
" </a:fontRef>\n"
" </p:style>\n"
" <p:txBody>\n"
' <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
" <a:lstStyle/>\n"
" <a:p>\n"
' <a:pPr algn="ctr"/>\n'
" </a:p>\n"
" </p:txBody>\n"
"</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
) % (shape_id, name, x, y, cx, cy)
return cast(CT_Shape, parse_xml(xml))
@staticmethod
def new_placeholder_sp(
id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx
) -> CT_Shape:
"""Return a new `p:sp` element tree configured as a placeholder shape."""
sp = cast(
CT_Shape,
parse_xml(
f"<p:sp {nsdecls('a', 'p')}>\n"
f" <p:nvSpPr>\n"
f' <p:cNvPr id="{id_}" name="{name}"/>\n'
f" <p:cNvSpPr>\n"
f' <a:spLocks noGrp="1"/>\n'
f" </p:cNvSpPr>\n"
f" <p:nvPr/>\n"
f" </p:nvSpPr>\n"
f" <p:spPr/>\n"
f"</p:sp>"
),
)
ph = sp.nvSpPr.nvPr.get_or_add_ph()
ph.type = ph_type
ph.idx = idx
ph.orient = orient
ph.sz = sz
placeholder_types_that_have_a_text_frame = (
PP_PLACEHOLDER.TITLE,
PP_PLACEHOLDER.CENTER_TITLE,
PP_PLACEHOLDER.SUBTITLE,
PP_PLACEHOLDER.BODY,
PP_PLACEHOLDER.OBJECT,
)
if ph_type in placeholder_types_that_have_a_text_frame:
sp.append(CT_TextBody.new())
return sp
@staticmethod
def new_textbox_sp(id_, name, left, top, width, height):
"""Return a new `p:sp` element tree configured as a base textbox shape."""
tmpl = CT_Shape._textbox_sp_tmpl()
xml = tmpl % (id_, name, left, top, width, height)
sp = parse_xml(xml)
return sp
@property
def prst(self):
"""Value of `prst` attribute of `a:prstGeom` element or |None| if not present."""
prstGeom = self.prstGeom
if prstGeom is None:
return None
return prstGeom.prst
@property
def prstGeom(self) -> CT_PresetGeometry2D:
"""Reference to `a:prstGeom` child element.
|None| if this shape doesn't have one, for example, if it's a placeholder shape.
"""
return self.spPr.prstGeom
def _new_txBody(self):
return CT_TextBody.new_p_txBody()
@staticmethod
def _textbox_sp_tmpl():
return (
"<p:sp %s>\n"
" <p:nvSpPr>\n"
' <p:cNvPr id="%s" name="%s"/>\n'
' <p:cNvSpPr txBox="1"/>\n'
" <p:nvPr/>\n"
" </p:nvSpPr>\n"
" <p:spPr>\n"
" <a:xfrm>\n"
' <a:off x="%s" y="%s"/>\n'
' <a:ext cx="%s" cy="%s"/>\n'
" </a:xfrm>\n"
' <a:prstGeom prst="rect">\n'
" <a:avLst/>\n"
" </a:prstGeom>\n"
" <a:noFill/>\n"
" </p:spPr>\n"
" <p:txBody>\n"
' <a:bodyPr wrap="none">\n'
" <a:spAutoFit/>\n"
" </a:bodyPr>\n"
" <a:lstStyle/>\n"
" <a:p/>\n"
" </p:txBody>\n"
"</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
)
class CT_ShapeNonVisual(BaseShapeElement):
"""`p:nvSpPr` custom element class."""
cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"p:cNvPr"
)
cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"p:cNvSpPr"
)
nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType]
OneAndOnlyOne("p:nvPr")
)
@@ -0,0 +1,107 @@
"""lxml custom element classes for XML elements related to the Connector shape."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.shapes.shared import BaseShapeElement
from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt
from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne
if TYPE_CHECKING:
from pptx.oxml.shapes.shared import CT_ShapeProperties
class CT_Connection(BaseShapeElement):
"""A `a:stCxn` or `a:endCxn` element.
Specifies a connection between an end-point of a connector and a shape connection point.
"""
id = RequiredAttribute("id", ST_DrawingElementId)
idx = RequiredAttribute("idx", XsdUnsignedInt)
class CT_Connector(BaseShapeElement):
"""A line/connector shape `p:cxnSp` element"""
_tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst")
nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr")
spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
del _tag_seq
@classmethod
def new_cxnSp(
cls,
id_: int,
name: str,
prst: str,
x: int,
y: int,
cx: int,
cy: int,
flipH: bool,
flipV: bool,
) -> CT_Connector:
"""Return a new `p:cxnSp` element tree configured as a base connector."""
flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "")
return cast(
CT_Connector,
parse_xml(
f"<p:cxnSp {nsdecls('a', 'p')}>\n"
f" <p:nvCxnSpPr>\n"
f' <p:cNvPr id="{id_}" name="{name}"/>\n'
f" <p:cNvCxnSpPr/>\n"
f" <p:nvPr/>\n"
f" </p:nvCxnSpPr>\n"
f" <p:spPr>\n"
f" <a:xfrm{flip}>\n"
f' <a:off x="{x}" y="{y}"/>\n'
f' <a:ext cx="{cx}" cy="{cy}"/>\n'
f" </a:xfrm>\n"
f' <a:prstGeom prst="{prst}">\n'
f" <a:avLst/>\n"
f" </a:prstGeom>\n"
f" </p:spPr>\n"
f" <p:style>\n"
f' <a:lnRef idx="2">\n'
f' <a:schemeClr val="accent1"/>\n'
f" </a:lnRef>\n"
f' <a:fillRef idx="0">\n'
f' <a:schemeClr val="accent1"/>\n'
f" </a:fillRef>\n"
f' <a:effectRef idx="1">\n'
f' <a:schemeClr val="accent1"/>\n'
f" </a:effectRef>\n"
f' <a:fontRef idx="minor">\n'
f' <a:schemeClr val="tx1"/>\n'
f" </a:fontRef>\n"
f" </p:style>\n"
f"</p:cxnSp>"
),
)
class CT_ConnectorNonVisual(BaseOxmlElement):
"""
`p:nvCxnSpPr` element, container for the non-visual properties of
a connector, such as name, id, etc.
"""
cNvPr = OneAndOnlyOne("p:cNvPr")
cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr")
nvPr = OneAndOnlyOne("p:nvPr")
class CT_NonVisualConnectorProperties(BaseOxmlElement):
"""
`p:cNvCxnSpPr` element, container for the non-visual properties specific
to a connector shape, such as connections and connector locking.
"""
_tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst")
stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:])
endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:])
del _tag_seq
@@ -0,0 +1,342 @@
"""lxml custom element class for CT_GraphicalObjectFrame XML element."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from pptx.oxml import parse_xml
from pptx.oxml.chart.chart import CT_Chart
from pptx.oxml.ns import nsdecls
from pptx.oxml.shapes.shared import BaseShapeElement
from pptx.oxml.simpletypes import XsdBoolean, XsdString
from pptx.oxml.table import CT_Table
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrOne,
)
from pptx.spec import (
GRAPHIC_DATA_URI_CHART,
GRAPHIC_DATA_URI_OLEOBJ,
GRAPHIC_DATA_URI_TABLE,
)
if TYPE_CHECKING:
from pptx.oxml.shapes.shared import (
CT_ApplicationNonVisualDrawingProps,
CT_NonVisualDrawingProps,
CT_Transform2D,
)
class CT_GraphicalObject(BaseOxmlElement):
"""`a:graphic` element.
The container for the reference to or definition of the framed graphical object (table, chart,
etc.).
"""
graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"a:graphicData"
)
@property
def chart(self) -> CT_Chart | None:
"""The `c:chart` grandchild element, or |None| if not present."""
return self.graphicData.chart
class CT_GraphicalObjectData(BaseShapeElement):
"""`p:graphicData` element.
The direct container for a table, a chart, or another graphical object.
"""
chart: CT_Chart | None = ZeroOrOne("c:chart") # pyright: ignore[reportAssignmentType]
tbl: CT_Table | None = ZeroOrOne("a:tbl") # pyright: ignore[reportAssignmentType]
uri: str = RequiredAttribute("uri", XsdString) # pyright: ignore[reportAssignmentType]
@property
def blob_rId(self) -> str | None:
"""Optional `r:id` attribute value of `p:oleObj` descendent element.
This value is `None` when this `p:graphicData` element does not enclose an OLE object.
This value could also be `None` if an enclosed OLE object does not specify this attribute
(it is specified optional in the schema) but so far, all OLE objects we've encountered
specify this value.
"""
return None if self._oleObj is None else self._oleObj.rId
@property
def is_embedded_ole_obj(self) -> bool | None:
"""Optional boolean indicating an embedded OLE object.
Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True`
indicates an embedded OLE object and `False` indicates a linked OLE object.
"""
return None if self._oleObj is None else self._oleObj.is_embedded
@property
def progId(self) -> str | None:
"""Optional str value of "progId" attribute of `p:oleObj` descendent.
This value identifies the "type" of the embedded object in terms of the application used
to open it.
This value is `None` when this `p:graphicData` element does not enclose an OLE object.
This could also be `None` if an enclosed OLE object does not specify this attribute (it is
specified optional in the schema) but so far, all OLE objects we've encountered specify
this value.
"""
return None if self._oleObj is None else self._oleObj.progId
@property
def showAsIcon(self) -> bool | None:
"""Optional value of "showAsIcon" attribute value of `p:oleObj` descendent.
This value is `None` when this `p:graphicData` element does not enclose an OLE object. It
is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element.
"""
return None if self._oleObj is None else self._oleObj.showAsIcon
@property
def _oleObj(self) -> CT_OleObject | None:
"""Optional `p:oleObj` element contained in this `p:graphicData' element.
Returns `None` when this graphic-data element does not enclose an OLE object. Note that
this returns the last `p:oleObj` element found. There can be more than one `p:oleObj`
element because an `mc.AlternateContent` element may appear as the child of
`p:graphicData` and that alternate-content subtree can contain multiple compatibility
choices. The last one should suit best for reading purposes because it contains the lowest
common denominator.
"""
oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj"))
return oleObjs[-1] if oleObjs else None
class CT_GraphicalObjectFrame(BaseShapeElement):
"""`p:graphicFrame` element.
A container for a table, a chart, or another graphical object.
"""
nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = ( # pyright: ignore[reportAssignmentType]
OneAndOnlyOne("p:nvGraphicFramePr")
)
xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm") # pyright: ignore
graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"a:graphic"
)
@property
def chart(self) -> CT_Chart | None:
"""The `c:chart` great-grandchild element, or |None| if not present."""
return self.graphic.chart
@property
def chart_rId(self) -> str | None:
"""The `rId` attribute of the `c:chart` great-grandchild element.
|None| if not present.
"""
chart = self.chart
if chart is None:
return None
return chart.rId
def get_or_add_xfrm(self) -> CT_Transform2D:
"""Return the required `p:xfrm` child element.
Overrides version on BaseShapeElement.
"""
return self.xfrm
@property
def graphicData(self) -> CT_GraphicalObjectData:
"""`a:graphicData` grandchild of this graphic-frame element."""
return self.graphic.graphicData
@property
def graphicData_uri(self) -> str:
"""str value of `uri` attribute of `a:graphicData` grandchild."""
return self.graphic.graphicData.uri
@property
def has_oleobj(self) -> bool:
"""`True` for graphicFrame containing an OLE object, `False` otherwise."""
return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ
@property
def is_embedded_ole_obj(self) -> bool | None:
"""Optional boolean indicating an embedded OLE object.
Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True`
indicates an embedded OLE object and `False` indicates a linked OLE object.
"""
return self.graphicData.is_embedded_ole_obj
@classmethod
def new_chart_graphicFrame(
cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int
) -> CT_GraphicalObjectFrame:
"""Return a `p:graphicFrame` element tree populated with a chart element."""
graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy)
graphicData = graphicFrame.graphic.graphicData
graphicData.uri = GRAPHIC_DATA_URI_CHART
graphicData.append(CT_Chart.new_chart(rId))
return graphicFrame
@classmethod
def new_graphicFrame(
cls, id_: int, name: str, x: int, y: int, cx: int, cy: int
) -> CT_GraphicalObjectFrame:
"""Return a new `p:graphicFrame` element tree suitable for containing a table or chart.
Note that a graphicFrame element is not a valid shape until it contains a graphical object
such as a table.
"""
return cast(
CT_GraphicalObjectFrame,
parse_xml(
f"<p:graphicFrame {nsdecls('a', 'p')}>\n"
f" <p:nvGraphicFramePr>\n"
f' <p:cNvPr id="{id_}" name="{name}"/>\n'
f" <p:cNvGraphicFramePr>\n"
f' <a:graphicFrameLocks noGrp="1"/>\n'
f" </p:cNvGraphicFramePr>\n"
f" <p:nvPr/>\n"
f" </p:nvGraphicFramePr>\n"
f" <p:xfrm>\n"
f' <a:off x="{x}" y="{y}"/>\n'
f' <a:ext cx="{cx}" cy="{cy}"/>\n'
f" </p:xfrm>\n"
f" <a:graphic>\n"
f" <a:graphicData/>\n"
f" </a:graphic>\n"
f"</p:graphicFrame>"
),
)
@classmethod
def new_ole_object_graphicFrame(
cls,
id_: int,
name: str,
ole_object_rId: str,
progId: str,
icon_rId: str,
x: int,
y: int,
cx: int,
cy: int,
imgW: int,
imgH: int,
) -> CT_GraphicalObjectFrame:
"""Return newly-created `p:graphicFrame` for embedded OLE-object.
`ole_object_rId` identifies the relationship to the OLE-object part.
`progId` is a str identifying the object-type in terms of the application (program) used
to open it. This becomes an attribute of the same name in the `p:oleObj` element.
`icon_rId` identifies the relationship to an image part used to display the OLE-object as
an icon (vs. a preview).
"""
return cast(
CT_GraphicalObjectFrame,
parse_xml(
f"<p:graphicFrame {nsdecls('a', 'p', 'r')}>\n"
f" <p:nvGraphicFramePr>\n"
f' <p:cNvPr id="{id_}" name="{name}"/>\n'
f" <p:cNvGraphicFramePr>\n"
f' <a:graphicFrameLocks noGrp="1"/>\n'
f" </p:cNvGraphicFramePr>\n"
f" <p:nvPr/>\n"
f" </p:nvGraphicFramePr>\n"
f" <p:xfrm>\n"
f' <a:off x="{x}" y="{y}"/>\n'
f' <a:ext cx="{cx}" cy="{cy}"/>\n'
f" </p:xfrm>\n"
f" <a:graphic>\n"
f" <a:graphicData"
f' uri="http://schemas.openxmlformats.org/presentationml/2006/ole">\n'
f' <p:oleObj showAsIcon="1"'
f' r:id="{ole_object_rId}"'
f' imgW="{imgW}"'
f' imgH="{imgH}"'
f' progId="{progId}">\n'
f" <p:embed/>\n"
f" <p:pic>\n"
f" <p:nvPicPr>\n"
f' <p:cNvPr id="0" name=""/>\n'
f" <p:cNvPicPr/>\n"
f" <p:nvPr/>\n"
f" </p:nvPicPr>\n"
f" <p:blipFill>\n"
f' <a:blip r:embed="{icon_rId}"/>\n'
f" <a:stretch>\n"
f" <a:fillRect/>\n"
f" </a:stretch>\n"
f" </p:blipFill>\n"
f" <p:spPr>\n"
f" <a:xfrm>\n"
f' <a:off x="{x}" y="{y}"/>\n'
f' <a:ext cx="{cx}" cy="{cy}"/>\n'
f" </a:xfrm>\n"
f' <a:prstGeom prst="rect">\n'
f" <a:avLst/>\n"
f" </a:prstGeom>\n"
f" </p:spPr>\n"
f" </p:pic>\n"
f" </p:oleObj>\n"
f" </a:graphicData>\n"
f" </a:graphic>\n"
f"</p:graphicFrame>"
),
)
@classmethod
def new_table_graphicFrame(
cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
) -> CT_GraphicalObjectFrame:
"""Return a `p:graphicFrame` element tree populated with a table element."""
graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy)
graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE
graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy))
return graphicFrame
class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement):
"""`p:nvGraphicFramePr` element.
This contains the non-visual properties of a graphic frame, such as name, id, etc.
"""
cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"p:cNvPr"
)
nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType]
OneAndOnlyOne("p:nvPr")
)
class CT_OleObject(BaseOxmlElement):
"""`p:oleObj` element, container for an OLE object (e.g. Excel file).
An OLE object can be either linked or embedded (hence the name).
"""
progId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"progId", XsdString
)
rId: str | None = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
showAsIcon: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"showAsIcon", XsdBoolean, default=False
)
@property
def is_embedded(self) -> bool:
"""True when this OLE object is embedded, False when it is linked."""
return len(self.xpath("./p:embed")) > 0
@@ -0,0 +1,280 @@
"""lxml custom element classes for shape-tree-related XML elements."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Iterator
from pptx.enum.shapes import MSO_CONNECTOR_TYPE
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls, qn
from pptx.oxml.shapes.autoshape import CT_Shape
from pptx.oxml.shapes.connector import CT_Connector
from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
from pptx.oxml.shapes.picture import CT_Picture
from pptx.oxml.shapes.shared import BaseShapeElement
from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne
from pptx.util import Emu
if TYPE_CHECKING:
from pptx.enum.shapes import PP_PLACEHOLDER
from pptx.oxml.shapes import ShapeElement
from pptx.oxml.shapes.shared import CT_Transform2D
class CT_GroupShape(BaseShapeElement):
"""Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements."""
nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"p:nvGrpSpPr"
)
grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"p:grpSpPr"
)
_shape_tags = (
qn("p:sp"),
qn("p:grpSp"),
qn("p:graphicFrame"),
qn("p:cxnSp"),
qn("p:pic"),
qn("p:contentPart"),
)
def add_autoshape(
self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int
) -> CT_Shape:
"""Return new `p:sp` appended to the group/shapetree with specified attributes."""
sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy)
self.insert_element_before(sp, "p:extLst")
return sp
def add_cxnSp(
self,
id_: int,
name: str,
type_member: MSO_CONNECTOR_TYPE,
x: int,
y: int,
cx: int,
cy: int,
flipH: bool,
flipV: bool,
) -> CT_Connector:
"""Return new `p:cxnSp` appended to the group/shapetree with the specified attribues."""
prst = MSO_CONNECTOR_TYPE.to_xml(type_member)
cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV)
self.insert_element_before(cxnSp, "p:extLst")
return cxnSp
def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape:
"""Append a new freeform `p:sp` with specified position and size."""
shape_id = self._next_shape_id
name = "Freeform %d" % (shape_id - 1,)
sp = CT_Shape.new_freeform_sp(shape_id, name, x, y, cx, cy)
self.insert_element_before(sp, "p:extLst")
return sp
def add_grpSp(self) -> CT_GroupShape:
"""Return `p:grpSp` element newly appended to this shape tree.
The element contains no sub-shapes, is positioned at (0, 0), and has
width and height of zero.
"""
shape_id = self._next_shape_id
name = "Group %d" % (shape_id - 1,)
grpSp = CT_GroupShape.new_grpSp(shape_id, name)
self.insert_element_before(grpSp, "p:extLst")
return grpSp
def add_pic(
self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int
) -> CT_Picture:
"""Append a `p:pic` shape to the group/shapetree having properties as specified in call."""
pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy)
self.insert_element_before(pic, "p:extLst")
return pic
def add_placeholder(
self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int
) -> CT_Shape:
"""Append a newly-created placeholder `p:sp` shape having the specified properties."""
sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx)
self.insert_element_before(sp, "p:extLst")
return sp
def add_table(
self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
) -> CT_GraphicalObjectFrame:
"""Append a `p:graphicFrame` shape containing a table as specified in call."""
graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame(
id_, name, rows, cols, x, y, cx, cy
)
self.insert_element_before(graphicFrame, "p:extLst")
return graphicFrame
def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape:
"""Append a newly-created textbox `p:sp` shape having the specified position and size."""
sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy)
self.insert_element_before(sp, "p:extLst")
return sp
@property
def chExt(self):
"""Descendent `p:grpSpPr/a:xfrm/a:chExt` element."""
return self.grpSpPr.get_or_add_xfrm().get_or_add_chExt()
@property
def chOff(self):
"""Descendent `p:grpSpPr/a:xfrm/a:chOff` element."""
return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff()
def get_or_add_xfrm(self) -> CT_Transform2D:
"""Return the `a:xfrm` grandchild element, newly-added if not present."""
return self.grpSpPr.get_or_add_xfrm()
def iter_ph_elms(self):
"""Generate each placeholder shape child element in document order."""
for e in self.iter_shape_elms():
if e.has_ph_elm:
yield e
def iter_shape_elms(self) -> Iterator[ShapeElement]:
"""Generate each child of this `p:spTree` element that corresponds to a shape.
Items appear in XML document order.
"""
for elm in self.iterchildren():
if elm.tag in self._shape_tags:
yield elm
@property
def max_shape_id(self) -> int:
"""Maximum int value assigned as @id in this slide.
This is generally a shape-id, but ids can be assigned to other
objects so we just check all @id values anywhere in the document
(XML id-values have document scope).
In practice, its minimum value is 1 because the spTree element itself
is always assigned id="1".
"""
id_str_lst = self.xpath("//@id")
used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
return max(used_ids) if used_ids else 0
@classmethod
def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape:
"""Return new "loose" `p:grpSp` element having `id_` and `name`."""
xml = (
"<p:grpSp %s>\n"
" <p:nvGrpSpPr>\n"
' <p:cNvPr id="%%d" name="%%s"/>\n'
" <p:cNvGrpSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvGrpSpPr>\n"
" <p:grpSpPr>\n"
" <a:xfrm>\n"
' <a:off x="0" y="0"/>\n'
' <a:ext cx="0" cy="0"/>\n'
' <a:chOff x="0" y="0"/>\n'
' <a:chExt cx="0" cy="0"/>\n'
" </a:xfrm>\n"
" </p:grpSpPr>\n"
"</p:grpSp>" % nsdecls("a", "p", "r")
) % (id_, name)
grpSp = parse_xml(xml)
return grpSp
def recalculate_extents(self) -> None:
"""Adjust x, y, cx, and cy to incorporate all contained shapes.
This would typically be called when a contained shape is added,
removed, or its position or size updated.
This method is recursive "upwards" since a change in a group shape
can change the position and size of its containing group.
"""
if not self.tag == qn("p:grpSp"):
return
x, y, cx, cy = self._child_extents
self.chOff.x = self.x = x
self.chOff.y = self.y = y
self.chExt.cx = self.cx = cx
self.chExt.cy = self.cy = cy
self.getparent().recalculate_extents()
@property
def xfrm(self) -> CT_Transform2D | None:
"""The `a:xfrm` grandchild element or |None| if not found."""
return self.grpSpPr.xfrm
@property
def _child_extents(self) -> tuple[int, int, int, int]:
"""(x, y, cx, cy) tuple representing net position and size.
The values are formed as a composite of the contained child shapes.
"""
child_shape_elms = list(self.iter_shape_elms())
if not child_shape_elms:
return Emu(0), Emu(0), Emu(0), Emu(0)
min_x = min([xSp.x for xSp in child_shape_elms])
min_y = min([xSp.y for xSp in child_shape_elms])
max_x = max([(xSp.x + xSp.cx) for xSp in child_shape_elms])
max_y = max([(xSp.y + xSp.cy) for xSp in child_shape_elms])
x = min_x
y = min_y
cx = max_x - min_x
cy = max_y - min_y
return x, y, cx, cy
@property
def _next_shape_id(self) -> int:
"""Return unique shape id suitable for use with a new shape element.
The returned id is the next available positive integer drawing object
id in shape tree, starting from 1 and making use of any gaps in
numbering. In practice, the minimum id is 2 because the spTree
element itself is always assigned id="1".
"""
id_str_lst = self.xpath("//@id")
used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
for n in range(1, len(used_ids) + 2):
if n not in used_ids:
return n
class CT_GroupShapeNonVisual(BaseShapeElement):
"""`p:nvGrpSpPr` element."""
cNvPr = OneAndOnlyOne("p:cNvPr")
class CT_GroupShapeProperties(BaseOxmlElement):
"""p:grpSpPr element"""
get_or_add_xfrm: Callable[[], CT_Transform2D]
_tag_seq = (
"a:xfrm",
"a:noFill",
"a:solidFill",
"a:gradFill",
"a:blipFill",
"a:pattFill",
"a:grpFill",
"a:effectLst",
"a:effectDag",
"a:scene3d",
"a:extLst",
)
xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:xfrm", successors=_tag_seq[1:]
)
effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:])
del _tag_seq
@@ -0,0 +1,270 @@
"""lxml custom element classes for picture-related XML elements."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from xml.sax.saxutils import escape
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.shapes.shared import BaseShapeElement
from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne
if TYPE_CHECKING:
from pptx.oxml.shapes.shared import CT_ShapeProperties
from pptx.util import Length
class CT_Picture(BaseShapeElement):
"""`p:pic` element.
Represents a picture shape (an image placement on a slide).
"""
nvPicPr = OneAndOnlyOne("p:nvPicPr")
blipFill = OneAndOnlyOne("p:blipFill")
spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
@property
def blip_rId(self) -> str | None:
"""Value of `p:blipFill/a:blip/@r:embed`.
Returns |None| if not present.
"""
blip = self.blipFill.blip
if blip is not None and blip.rEmbed is not None:
return blip.rEmbed
return None
def crop_to_fit(self, image_size, view_size):
"""
Set cropping values in `p:blipFill/a:srcRect` such that an image of
*image_size* will stretch to exactly fit *view_size* when its aspect
ratio is preserved.
"""
self.blipFill.crop(self._fill_cropping(image_size, view_size))
def get_or_add_ln(self):
"""
Return the <a:ln> grandchild element, newly added if not present.
"""
return self.spPr.get_or_add_ln()
@property
def ln(self):
"""
``<a:ln>`` grand-child element or |None| if not present
"""
return self.spPr.ln
@classmethod
def new_ph_pic(cls, id_, name, desc, rId):
"""
Return a new `p:pic` placeholder element populated with the supplied
parameters.
"""
return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId))
@classmethod
def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy):
"""Return new `<p:pic>` element tree configured with supplied parameters."""
return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy))
@classmethod
def new_video_pic(
cls,
shape_id: int,
shape_name: str,
video_rId: str,
media_rId: str,
poster_frame_rId: str,
x: Length,
y: Length,
cx: Length,
cy: Length,
) -> CT_Picture:
"""Return a new `p:pic` populated with the specified video."""
return cast(
CT_Picture,
parse_xml(
cls._pic_video_tmpl()
% (
shape_id,
shape_name,
video_rId,
media_rId,
poster_frame_rId,
x,
y,
cx,
cy,
)
),
)
@property
def srcRect_b(self):
"""Value of `p:blipFill/a:srcRect/@b` or 0.0 if not present."""
return self._srcRect_x("b")
@srcRect_b.setter
def srcRect_b(self, value):
self.blipFill.get_or_add_srcRect().b = value
@property
def srcRect_l(self):
"""Value of `p:blipFill/a:srcRect/@l` or 0.0 if not present."""
return self._srcRect_x("l")
@srcRect_l.setter
def srcRect_l(self, value):
self.blipFill.get_or_add_srcRect().l = value # noqa
@property
def srcRect_r(self):
"""Value of `p:blipFill/a:srcRect/@r` or 0.0 if not present."""
return self._srcRect_x("r")
@srcRect_r.setter
def srcRect_r(self, value):
self.blipFill.get_or_add_srcRect().r = value
@property
def srcRect_t(self):
"""Value of `p:blipFill/a:srcRect/@t` or 0.0 if not present."""
return self._srcRect_x("t")
@srcRect_t.setter
def srcRect_t(self, value):
self.blipFill.get_or_add_srcRect().t = value
def _fill_cropping(self, image_size, view_size):
"""
Return a (left, top, right, bottom) 4-tuple containing the cropping
values required to display an image of *image_size* in *view_size*
when stretched proportionately. Each value is a percentage expressed
as a fraction of 1.0, e.g. 0.425 represents 42.5%. *image_size* and
*view_size* are each (width, height) pairs.
"""
def aspect_ratio(width, height):
return width / height
ar_view = aspect_ratio(*view_size)
ar_image = aspect_ratio(*image_size)
if ar_view < ar_image: # image too wide
crop = (1.0 - (ar_view / ar_image)) / 2.0
return (crop, 0.0, crop, 0.0)
if ar_view > ar_image: # image too tall
crop = (1.0 - (ar_image / ar_view)) / 2.0
return (0.0, crop, 0.0, crop)
return (0.0, 0.0, 0.0, 0.0)
@classmethod
def _pic_ph_tmpl(cls):
return (
"<p:pic %s>\n"
" <p:nvPicPr>\n"
' <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
" <p:cNvPicPr>\n"
' <a:picLocks noGrp="1" noChangeAspect="1"/>\n'
" </p:cNvPicPr>\n"
" <p:nvPr/>\n"
" </p:nvPicPr>\n"
" <p:blipFill>\n"
' <a:blip r:embed="%%s"/>\n'
" <a:stretch>\n"
" <a:fillRect/>\n"
" </a:stretch>\n"
" </p:blipFill>\n"
" <p:spPr/>\n"
"</p:pic>" % nsdecls("p", "a", "r")
)
@classmethod
def _pic_tmpl(cls):
return (
"<p:pic %s>\n"
" <p:nvPicPr>\n"
' <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
" <p:cNvPicPr>\n"
' <a:picLocks noChangeAspect="1"/>\n'
" </p:cNvPicPr>\n"
" <p:nvPr/>\n"
" </p:nvPicPr>\n"
" <p:blipFill>\n"
' <a:blip r:embed="%%s"/>\n'
" <a:stretch>\n"
" <a:fillRect/>\n"
" </a:stretch>\n"
" </p:blipFill>\n"
" <p:spPr>\n"
" <a:xfrm>\n"
' <a:off x="%%d" y="%%d"/>\n'
' <a:ext cx="%%d" cy="%%d"/>\n'
" </a:xfrm>\n"
' <a:prstGeom prst="rect">\n'
" <a:avLst/>\n"
" </a:prstGeom>\n"
" </p:spPr>\n"
"</p:pic>" % nsdecls("a", "p", "r")
)
@classmethod
def _pic_video_tmpl(cls):
return (
"<p:pic %s>\n"
" <p:nvPicPr>\n"
' <p:cNvPr id="%%d" name="%%s">\n'
' <a:hlinkClick r:id="" action="ppaction://media"/>\n'
" </p:cNvPr>\n"
" <p:cNvPicPr>\n"
' <a:picLocks noChangeAspect="1"/>\n'
" </p:cNvPicPr>\n"
" <p:nvPr>\n"
' <a:videoFile r:link="%%s"/>\n'
" <p:extLst>\n"
' <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">\n'
' <p14:media xmlns:p14="http://schemas.microsoft.com/of'
'fice/powerpoint/2010/main" r:embed="%%s"/>\n'
" </p:ext>\n"
" </p:extLst>\n"
" </p:nvPr>\n"
" </p:nvPicPr>\n"
" <p:blipFill>\n"
' <a:blip r:embed="%%s"/>\n'
" <a:stretch>\n"
" <a:fillRect/>\n"
" </a:stretch>\n"
" </p:blipFill>\n"
" <p:spPr>\n"
" <a:xfrm>\n"
' <a:off x="%%d" y="%%d"/>\n'
' <a:ext cx="%%d" cy="%%d"/>\n'
" </a:xfrm>\n"
' <a:prstGeom prst="rect">\n'
" <a:avLst/>\n"
" </a:prstGeom>\n"
" </p:spPr>\n"
"</p:pic>" % nsdecls("a", "p", "r")
)
def _srcRect_x(self, attr_name):
"""
Value of `p:blipFill/a:srcRect/@{attr_name}` or 0.0 if not present.
"""
srcRect = self.blipFill.srcRect
if srcRect is None:
return 0.0
return getattr(srcRect, attr_name)
class CT_PictureNonVisual(BaseOxmlElement):
"""
``<p:nvPicPr>`` element, containing non-visual properties for a picture
shape.
"""
cNvPr = OneAndOnlyOne("p:cNvPr")
nvPr = OneAndOnlyOne("p:nvPr")
@@ -0,0 +1,523 @@
"""Common shape-related oxml objects."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable
from pptx.dml.fill import CT_GradientFillProperties
from pptx.enum.shapes import PP_PLACEHOLDER
from pptx.oxml.ns import qn
from pptx.oxml.simpletypes import (
ST_Angle,
ST_Coordinate,
ST_Direction,
ST_DrawingElementId,
ST_LineWidth,
ST_PlaceholderSize,
ST_PositiveCoordinate,
XsdBoolean,
XsdString,
XsdUnsignedInt,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OptionalAttribute,
OxmlElement,
RequiredAttribute,
ZeroOrOne,
ZeroOrOneChoice,
)
from pptx.util import Emu
if TYPE_CHECKING:
from pptx.oxml.action import CT_Hyperlink
from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D
from pptx.util import Length
class BaseShapeElement(BaseOxmlElement):
"""Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc."""
spPr: CT_ShapeProperties
@property
def cx(self) -> Length:
return self._get_xfrm_attr("cx")
@cx.setter
def cx(self, value):
self._set_xfrm_attr("cx", value)
@property
def cy(self) -> Length:
return self._get_xfrm_attr("cy")
@cy.setter
def cy(self, value):
self._set_xfrm_attr("cy", value)
@property
def flipH(self):
return bool(self._get_xfrm_attr("flipH"))
@flipH.setter
def flipH(self, value):
self._set_xfrm_attr("flipH", value)
@property
def flipV(self):
return bool(self._get_xfrm_attr("flipV"))
@flipV.setter
def flipV(self, value):
self._set_xfrm_attr("flipV", value)
def get_or_add_xfrm(self):
"""Return the `a:xfrm` grandchild element, newly-added if not present.
This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
override.
"""
return self.spPr.get_or_add_xfrm()
@property
def has_ph_elm(self):
"""
True if this shape element has a `p:ph` descendant, indicating it
is a placeholder shape. False otherwise.
"""
return self.ph is not None
@property
def ph(self) -> CT_Placeholder | None:
"""The `p:ph` descendant element if there is one, None otherwise."""
ph_elms = self.xpath("./*[1]/p:nvPr/p:ph")
if len(ph_elms) == 0:
return None
return ph_elms[0]
@property
def ph_idx(self) -> int:
"""Integer value of placeholder idx attribute.
Raises |ValueError| if shape is not a placeholder.
"""
ph = self.ph
if ph is None:
raise ValueError("not a placeholder shape")
return ph.idx
@property
def ph_orient(self) -> str:
"""Placeholder orientation, e.g. 'vert'.
Raises |ValueError| if shape is not a placeholder.
"""
ph = self.ph
if ph is None:
raise ValueError("not a placeholder shape")
return ph.orient
@property
def ph_sz(self) -> str:
"""Placeholder size, e.g. ST_PlaceholderSize.HALF.
Raises `ValueError` if shape is not a placeholder.
"""
ph = self.ph
if ph is None:
raise ValueError("not a placeholder shape")
return ph.sz
@property
def ph_type(self):
"""Placeholder type, e.g. ST_PlaceholderType.TITLE ('title').
Raises `ValueError` if shape is not a placeholder.
"""
ph = self.ph
if ph is None:
raise ValueError("not a placeholder shape")
return ph.type
@property
def rot(self) -> float:
"""Float representing degrees this shape is rotated clockwise."""
xfrm = self.xfrm
if xfrm is None or xfrm.rot is None:
return 0.0
return xfrm.rot
@rot.setter
def rot(self, value: float):
self.get_or_add_xfrm().rot = value
@property
def shape_id(self):
"""
Integer id of this shape
"""
return self._nvXxPr.cNvPr.id
@property
def shape_name(self):
"""
Name of this shape
"""
return self._nvXxPr.cNvPr.name
@property
def txBody(self):
"""Child `p:txBody` element, None if not present."""
return self.find(qn("p:txBody"))
@property
def x(self) -> Length:
return self._get_xfrm_attr("x")
@x.setter
def x(self, value):
self._set_xfrm_attr("x", value)
@property
def xfrm(self):
"""The `a:xfrm` grandchild element or |None| if not found.
This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
override.
"""
return self.spPr.xfrm
@property
def y(self) -> Length:
return self._get_xfrm_attr("y")
@y.setter
def y(self, value):
self._set_xfrm_attr("y", value)
@property
def _nvXxPr(self):
"""
Required non-visual shape properties element for this shape. Actual
name depends on the shape type, e.g. `p:nvPicPr` for picture
shape.
"""
return self.xpath("./*[1]")[0]
def _get_xfrm_attr(self, name: str) -> Length | None:
xfrm = self.xfrm
if xfrm is None:
return None
return getattr(xfrm, name)
def _set_xfrm_attr(self, name, value):
xfrm = self.get_or_add_xfrm()
setattr(xfrm, name, value)
class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement):
"""`p:nvPr` element."""
get_or_add_ph: Callable[[], CT_Placeholder]
ph = ZeroOrOne(
"p:ph",
successors=(
"a:audioCd",
"a:wavAudioFile",
"a:audioFile",
"a:videoFile",
"a:quickTimeFile",
"p:custDataLst",
"p:extLst",
),
)
class CT_LineProperties(BaseOxmlElement):
"""Custom element class for <a:ln> element"""
_tag_seq = (
"a:noFill",
"a:solidFill",
"a:gradFill",
"a:pattFill",
"a:prstDash",
"a:custDash",
"a:round",
"a:bevel",
"a:miter",
"a:headEnd",
"a:tailEnd",
"a:extLst",
)
eg_lineFillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:pattFill"),
),
successors=_tag_seq[4:],
)
prstDash = ZeroOrOne("a:prstDash", successors=_tag_seq[5:])
custDash = ZeroOrOne("a:custDash", successors=_tag_seq[6:])
del _tag_seq
w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
@property
def eg_fillProperties(self):
"""
Required to fulfill the interface used by dml.fill.
"""
return self.eg_lineFillProperties
@property
def prstDash_val(self):
"""Return value of `val` attribute of `a:prstDash` child.
Return |None| if not present.
"""
prstDash = self.prstDash
if prstDash is None:
return None
return prstDash.val
@prstDash_val.setter
def prstDash_val(self, val):
self._remove_custDash()
prstDash = self.get_or_add_prstDash()
prstDash.val = val
class CT_NonVisualDrawingProps(BaseOxmlElement):
"""`p:cNvPr` custom element class."""
get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
get_or_add_hlinkHover: Callable[[], CT_Hyperlink]
_tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst")
hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:])
hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:])
id = RequiredAttribute("id", ST_DrawingElementId)
name = RequiredAttribute("name", XsdString)
del _tag_seq
class CT_Placeholder(BaseOxmlElement):
"""`p:ph` custom element class."""
type: PP_PLACEHOLDER = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT
)
orient: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"orient", ST_Direction, default=ST_Direction.HORZ
)
sz: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL
)
idx: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"idx", XsdUnsignedInt, default=0
)
class CT_Point2D(BaseOxmlElement):
"""
Custom element class for <a:off> element.
"""
x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType]
y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType]
class CT_PositiveSize2D(BaseOxmlElement):
"""
Custom element class for <a:ext> element.
"""
cx = RequiredAttribute("cx", ST_PositiveCoordinate)
cy = RequiredAttribute("cy", ST_PositiveCoordinate)
class CT_ShapeProperties(BaseOxmlElement):
"""Custom element class for `p:spPr` element.
Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more obscure ones.
"""
get_or_add_xfrm: Callable[[], CT_Transform2D]
get_or_add_ln: Callable[[], CT_LineProperties]
_add_prstGeom: Callable[[], CT_PresetGeometry2D]
_remove_custGeom: Callable[[], None]
_tag_seq = (
"a:xfrm",
"a:custGeom",
"a:prstGeom",
"a:noFill",
"a:solidFill",
"a:gradFill",
"a:blipFill",
"a:pattFill",
"a:grpFill",
"a:ln",
"a:effectLst",
"a:effectDag",
"a:scene3d",
"a:sp3d",
"a:extLst",
)
xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:xfrm", successors=_tag_seq[1:]
)
custGeom: CT_CustomGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:custGeom", successors=_tag_seq[2:]
)
prstGeom: CT_PresetGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:prstGeom", successors=_tag_seq[3:]
)
eg_fillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:blipFill"),
Choice("a:pattFill"),
Choice("a:grpFill"),
),
successors=_tag_seq[9:],
)
ln: CT_LineProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:ln", successors=_tag_seq[10:]
)
effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:])
del _tag_seq
@property
def cx(self):
"""
Shape width as an instance of Emu, or None if not present.
"""
cx_str_lst = self.xpath("./a:xfrm/a:ext/@cx")
if not cx_str_lst:
return None
return Emu(cx_str_lst[0])
@property
def cy(self):
"""
Shape height as an instance of Emu, or None if not present.
"""
cy_str_lst = self.xpath("./a:xfrm/a:ext/@cy")
if not cy_str_lst:
return None
return Emu(cy_str_lst[0])
@property
def x(self) -> Length | None:
"""Distance between the left edge of the slide and left edge of the shape.
0 if not present.
"""
x_str_lst = self.xpath("./a:xfrm/a:off/@x")
if not x_str_lst:
return None
return Emu(x_str_lst[0])
@property
def y(self):
"""
The offset of the top of the shape from the top of the slide, as an
instance of Emu. None if not present.
"""
y_str_lst = self.xpath("./a:xfrm/a:off/@y")
if not y_str_lst:
return None
return Emu(y_str_lst[0])
def _new_gradFill(self):
return CT_GradientFillProperties.new_gradFill()
class CT_Transform2D(BaseOxmlElement):
"""`a:xfrm` custom element class.
NOTE: this is a composite including CT_GroupTransform2D, which appears
with the `a:xfrm` tag in a group shape (including a slide `p:spTree`).
"""
_tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt")
off: CT_Point2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:off", successors=_tag_seq[1:]
)
ext = ZeroOrOne("a:ext", successors=_tag_seq[2:])
chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:])
chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:])
del _tag_seq
rot: float | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"rot", ST_Angle, default=0.0
)
flipH = OptionalAttribute("flipH", XsdBoolean, default=False)
flipV = OptionalAttribute("flipV", XsdBoolean, default=False)
@property
def x(self):
off = self.off
if off is None:
return None
return off.x
@x.setter
def x(self, value):
off = self.get_or_add_off()
off.x = value
@property
def y(self):
off = self.off
if off is None:
return None
return off.y
@y.setter
def y(self, value):
off = self.get_or_add_off()
off.y = value
@property
def cx(self):
ext = self.ext
if ext is None:
return None
return ext.cx
@cx.setter
def cx(self, value):
ext = self.get_or_add_ext()
ext.cx = value
@property
def cy(self):
ext = self.ext
if ext is None:
return None
return ext.cy
@cy.setter
def cy(self, value):
ext = self.get_or_add_ext()
ext.cy = value
def _new_ext(self):
ext = OxmlElement("a:ext")
ext.cx = 0
ext.cy = 0
return ext
def _new_off(self):
off = OxmlElement("a:off")
off.x = 0
off.y = 0
return off