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,486 @@
"""Initializes lxml parser, particularly the custom element classes.
Also makes available a handful of functions that wrap its typical uses.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Type
from lxml import etree
from pptx.oxml.ns import NamespacePrefixedTag
if TYPE_CHECKING:
from pptx.oxml.xmlchemy import BaseOxmlElement
# -- configure etree XML parser ----------------------------
element_class_lookup = etree.ElementNamespaceClassLookup()
oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
oxml_parser.set_element_class_lookup(element_class_lookup)
def parse_from_template(template_file_name: str):
"""Return an element loaded from the XML in the template file identified by `template_name`."""
thisdir = os.path.split(__file__)[0]
filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_file_name)
with open(filename, "rb") as f:
xml = f.read()
return parse_xml(xml)
def parse_xml(xml: str | bytes):
"""Return root lxml element obtained by parsing XML character string in `xml`."""
return etree.fromstring(xml, oxml_parser)
def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
"""Register `cls` to be constructed when oxml parser encounters element having `nsptag_name`.
`nsptag_name` is a string of the form `nspfx:tagroot`, e.g. `"w:document"`.
"""
nsptag = NamespacePrefixedTag(nsptagname)
namespace = element_class_lookup.get_namespace(nsptag.nsuri)
namespace[nsptag.local_part] = cls
from pptx.oxml.action import CT_Hyperlink # noqa: E402
register_element_cls("a:hlinkClick", CT_Hyperlink)
register_element_cls("a:hlinkHover", CT_Hyperlink)
from pptx.oxml.chart.axis import ( # noqa: E402
CT_AxisUnit,
CT_CatAx,
CT_ChartLines,
CT_Crosses,
CT_DateAx,
CT_LblOffset,
CT_Orientation,
CT_Scaling,
CT_TickLblPos,
CT_TickMark,
CT_ValAx,
)
register_element_cls("c:catAx", CT_CatAx)
register_element_cls("c:crosses", CT_Crosses)
register_element_cls("c:dateAx", CT_DateAx)
register_element_cls("c:lblOffset", CT_LblOffset)
register_element_cls("c:majorGridlines", CT_ChartLines)
register_element_cls("c:majorTickMark", CT_TickMark)
register_element_cls("c:majorUnit", CT_AxisUnit)
register_element_cls("c:minorTickMark", CT_TickMark)
register_element_cls("c:minorUnit", CT_AxisUnit)
register_element_cls("c:orientation", CT_Orientation)
register_element_cls("c:scaling", CT_Scaling)
register_element_cls("c:tickLblPos", CT_TickLblPos)
register_element_cls("c:valAx", CT_ValAx)
from pptx.oxml.chart.chart import ( # noqa: E402
CT_Chart,
CT_ChartSpace,
CT_ExternalData,
CT_PlotArea,
CT_Style,
)
register_element_cls("c:chart", CT_Chart)
register_element_cls("c:chartSpace", CT_ChartSpace)
register_element_cls("c:externalData", CT_ExternalData)
register_element_cls("c:plotArea", CT_PlotArea)
register_element_cls("c:style", CT_Style)
from pptx.oxml.chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402
register_element_cls("c:dLbl", CT_DLbl)
register_element_cls("c:dLblPos", CT_DLblPos)
register_element_cls("c:dLbls", CT_DLbls)
from pptx.oxml.chart.legend import CT_Legend, CT_LegendPos # noqa: E402
register_element_cls("c:legend", CT_Legend)
register_element_cls("c:legendPos", CT_LegendPos)
from pptx.oxml.chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402
register_element_cls("c:marker", CT_Marker)
register_element_cls("c:size", CT_MarkerSize)
register_element_cls("c:symbol", CT_MarkerStyle)
from pptx.oxml.chart.plot import ( # noqa: E402
CT_Area3DChart,
CT_AreaChart,
CT_BarChart,
CT_BarDir,
CT_BubbleChart,
CT_BubbleScale,
CT_DoughnutChart,
CT_GapAmount,
CT_Grouping,
CT_LineChart,
CT_Overlap,
CT_PieChart,
CT_RadarChart,
CT_ScatterChart,
)
register_element_cls("c:area3DChart", CT_Area3DChart)
register_element_cls("c:areaChart", CT_AreaChart)
register_element_cls("c:barChart", CT_BarChart)
register_element_cls("c:barDir", CT_BarDir)
register_element_cls("c:bubbleChart", CT_BubbleChart)
register_element_cls("c:bubbleScale", CT_BubbleScale)
register_element_cls("c:doughnutChart", CT_DoughnutChart)
register_element_cls("c:gapWidth", CT_GapAmount)
register_element_cls("c:grouping", CT_Grouping)
register_element_cls("c:lineChart", CT_LineChart)
register_element_cls("c:overlap", CT_Overlap)
register_element_cls("c:pieChart", CT_PieChart)
register_element_cls("c:radarChart", CT_RadarChart)
register_element_cls("c:scatterChart", CT_ScatterChart)
from pptx.oxml.chart.series import ( # noqa: E402
CT_AxDataSource,
CT_DPt,
CT_Lvl,
CT_NumDataSource,
CT_SeriesComposite,
CT_StrVal_NumVal_Composite,
)
register_element_cls("c:bubbleSize", CT_NumDataSource)
register_element_cls("c:cat", CT_AxDataSource)
register_element_cls("c:dPt", CT_DPt)
register_element_cls("c:lvl", CT_Lvl)
register_element_cls("c:pt", CT_StrVal_NumVal_Composite)
register_element_cls("c:ser", CT_SeriesComposite)
register_element_cls("c:val", CT_NumDataSource)
register_element_cls("c:xVal", CT_NumDataSource)
register_element_cls("c:yVal", CT_NumDataSource)
from pptx.oxml.chart.shared import ( # noqa: E402
CT_Boolean,
CT_Boolean_Explicit,
CT_Double,
CT_Layout,
CT_LayoutMode,
CT_ManualLayout,
CT_NumFmt,
CT_Title,
CT_Tx,
CT_UnsignedInt,
)
register_element_cls("c:autoTitleDeleted", CT_Boolean_Explicit)
register_element_cls("c:autoUpdate", CT_Boolean)
register_element_cls("c:bubble3D", CT_Boolean)
register_element_cls("c:crossAx", CT_UnsignedInt)
register_element_cls("c:crossesAt", CT_Double)
register_element_cls("c:date1904", CT_Boolean)
register_element_cls("c:delete", CT_Boolean)
register_element_cls("c:idx", CT_UnsignedInt)
register_element_cls("c:invertIfNegative", CT_Boolean_Explicit)
register_element_cls("c:layout", CT_Layout)
register_element_cls("c:manualLayout", CT_ManualLayout)
register_element_cls("c:max", CT_Double)
register_element_cls("c:min", CT_Double)
register_element_cls("c:numFmt", CT_NumFmt)
register_element_cls("c:order", CT_UnsignedInt)
register_element_cls("c:overlay", CT_Boolean_Explicit)
register_element_cls("c:ptCount", CT_UnsignedInt)
register_element_cls("c:showCatName", CT_Boolean_Explicit)
register_element_cls("c:showLegendKey", CT_Boolean_Explicit)
register_element_cls("c:showPercent", CT_Boolean_Explicit)
register_element_cls("c:showSerName", CT_Boolean_Explicit)
register_element_cls("c:showVal", CT_Boolean_Explicit)
register_element_cls("c:smooth", CT_Boolean)
register_element_cls("c:title", CT_Title)
register_element_cls("c:tx", CT_Tx)
register_element_cls("c:varyColors", CT_Boolean)
register_element_cls("c:x", CT_Double)
register_element_cls("c:xMode", CT_LayoutMode)
from pptx.oxml.coreprops import CT_CoreProperties # noqa: E402
register_element_cls("cp:coreProperties", CT_CoreProperties)
from pptx.oxml.dml.color import ( # noqa: E402
CT_Color,
CT_HslColor,
CT_Percentage,
CT_PresetColor,
CT_SchemeColor,
CT_ScRgbColor,
CT_SRgbColor,
CT_SystemColor,
)
register_element_cls("a:bgClr", CT_Color)
register_element_cls("a:fgClr", CT_Color)
register_element_cls("a:hslClr", CT_HslColor)
register_element_cls("a:lumMod", CT_Percentage)
register_element_cls("a:lumOff", CT_Percentage)
register_element_cls("a:prstClr", CT_PresetColor)
register_element_cls("a:schemeClr", CT_SchemeColor)
register_element_cls("a:scrgbClr", CT_ScRgbColor)
register_element_cls("a:srgbClr", CT_SRgbColor)
register_element_cls("a:sysClr", CT_SystemColor)
from pptx.oxml.dml.fill import ( # noqa: E402
CT_Blip,
CT_BlipFillProperties,
CT_GradientFillProperties,
CT_GradientStop,
CT_GradientStopList,
CT_GroupFillProperties,
CT_LinearShadeProperties,
CT_NoFillProperties,
CT_PatternFillProperties,
CT_RelativeRect,
CT_SolidColorFillProperties,
)
register_element_cls("a:blip", CT_Blip)
register_element_cls("a:blipFill", CT_BlipFillProperties)
register_element_cls("a:gradFill", CT_GradientFillProperties)
register_element_cls("a:grpFill", CT_GroupFillProperties)
register_element_cls("a:gs", CT_GradientStop)
register_element_cls("a:gsLst", CT_GradientStopList)
register_element_cls("a:lin", CT_LinearShadeProperties)
register_element_cls("a:noFill", CT_NoFillProperties)
register_element_cls("a:pattFill", CT_PatternFillProperties)
register_element_cls("a:solidFill", CT_SolidColorFillProperties)
register_element_cls("a:srcRect", CT_RelativeRect)
from pptx.oxml.dml.line import CT_PresetLineDashProperties # noqa: E402
register_element_cls("a:prstDash", CT_PresetLineDashProperties)
from pptx.oxml.presentation import ( # noqa: E402
CT_Presentation,
CT_SlideId,
CT_SlideIdList,
CT_SlideMasterIdList,
CT_SlideMasterIdListEntry,
CT_SlideSize,
)
register_element_cls("p:presentation", CT_Presentation)
register_element_cls("p:sldId", CT_SlideId)
register_element_cls("p:sldIdLst", CT_SlideIdList)
register_element_cls("p:sldMasterId", CT_SlideMasterIdListEntry)
register_element_cls("p:sldMasterIdLst", CT_SlideMasterIdList)
register_element_cls("p:sldSz", CT_SlideSize)
from pptx.oxml.shapes.autoshape import ( # noqa: E402
CT_AdjPoint2D,
CT_CustomGeometry2D,
CT_GeomGuide,
CT_GeomGuideList,
CT_NonVisualDrawingShapeProps,
CT_Path2D,
CT_Path2DClose,
CT_Path2DLineTo,
CT_Path2DList,
CT_Path2DMoveTo,
CT_PresetGeometry2D,
CT_Shape,
CT_ShapeNonVisual,
)
register_element_cls("a:avLst", CT_GeomGuideList)
register_element_cls("a:custGeom", CT_CustomGeometry2D)
register_element_cls("a:gd", CT_GeomGuide)
register_element_cls("a:close", CT_Path2DClose)
register_element_cls("a:lnTo", CT_Path2DLineTo)
register_element_cls("a:moveTo", CT_Path2DMoveTo)
register_element_cls("a:path", CT_Path2D)
register_element_cls("a:pathLst", CT_Path2DList)
register_element_cls("a:prstGeom", CT_PresetGeometry2D)
register_element_cls("a:pt", CT_AdjPoint2D)
register_element_cls("p:cNvSpPr", CT_NonVisualDrawingShapeProps)
register_element_cls("p:nvSpPr", CT_ShapeNonVisual)
register_element_cls("p:sp", CT_Shape)
from pptx.oxml.shapes.connector import ( # noqa: E402
CT_Connection,
CT_Connector,
CT_ConnectorNonVisual,
CT_NonVisualConnectorProperties,
)
register_element_cls("a:endCxn", CT_Connection)
register_element_cls("a:stCxn", CT_Connection)
register_element_cls("p:cNvCxnSpPr", CT_NonVisualConnectorProperties)
register_element_cls("p:cxnSp", CT_Connector)
register_element_cls("p:nvCxnSpPr", CT_ConnectorNonVisual)
from pptx.oxml.shapes.graphfrm import ( # noqa: E402
CT_GraphicalObject,
CT_GraphicalObjectData,
CT_GraphicalObjectFrame,
CT_GraphicalObjectFrameNonVisual,
CT_OleObject,
)
register_element_cls("a:graphic", CT_GraphicalObject)
register_element_cls("a:graphicData", CT_GraphicalObjectData)
register_element_cls("p:graphicFrame", CT_GraphicalObjectFrame)
register_element_cls("p:nvGraphicFramePr", CT_GraphicalObjectFrameNonVisual)
register_element_cls("p:oleObj", CT_OleObject)
from pptx.oxml.shapes.groupshape import ( # noqa: E402
CT_GroupShape,
CT_GroupShapeNonVisual,
CT_GroupShapeProperties,
)
register_element_cls("p:grpSp", CT_GroupShape)
register_element_cls("p:grpSpPr", CT_GroupShapeProperties)
register_element_cls("p:nvGrpSpPr", CT_GroupShapeNonVisual)
register_element_cls("p:spTree", CT_GroupShape)
from pptx.oxml.shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402
register_element_cls("p:blipFill", CT_BlipFillProperties)
register_element_cls("p:nvPicPr", CT_PictureNonVisual)
register_element_cls("p:pic", CT_Picture)
from pptx.oxml.shapes.shared import ( # noqa: E402
CT_ApplicationNonVisualDrawingProps,
CT_LineProperties,
CT_NonVisualDrawingProps,
CT_Placeholder,
CT_Point2D,
CT_PositiveSize2D,
CT_ShapeProperties,
CT_Transform2D,
)
register_element_cls("a:chExt", CT_PositiveSize2D)
register_element_cls("a:chOff", CT_Point2D)
register_element_cls("a:ext", CT_PositiveSize2D)
register_element_cls("a:ln", CT_LineProperties)
register_element_cls("a:off", CT_Point2D)
register_element_cls("a:xfrm", CT_Transform2D)
register_element_cls("c:spPr", CT_ShapeProperties)
register_element_cls("p:cNvPr", CT_NonVisualDrawingProps)
register_element_cls("p:nvPr", CT_ApplicationNonVisualDrawingProps)
register_element_cls("p:ph", CT_Placeholder)
register_element_cls("p:spPr", CT_ShapeProperties)
register_element_cls("p:xfrm", CT_Transform2D)
from pptx.oxml.slide import ( # noqa: E402
CT_Background,
CT_BackgroundProperties,
CT_CommonSlideData,
CT_NotesMaster,
CT_NotesSlide,
CT_Slide,
CT_SlideLayout,
CT_SlideLayoutIdList,
CT_SlideLayoutIdListEntry,
CT_SlideMaster,
CT_SlideTiming,
CT_TimeNodeList,
CT_TLMediaNodeVideo,
)
register_element_cls("p:bg", CT_Background)
register_element_cls("p:bgPr", CT_BackgroundProperties)
register_element_cls("p:childTnLst", CT_TimeNodeList)
register_element_cls("p:cSld", CT_CommonSlideData)
register_element_cls("p:notes", CT_NotesSlide)
register_element_cls("p:notesMaster", CT_NotesMaster)
register_element_cls("p:sld", CT_Slide)
register_element_cls("p:sldLayout", CT_SlideLayout)
register_element_cls("p:sldLayoutId", CT_SlideLayoutIdListEntry)
register_element_cls("p:sldLayoutIdLst", CT_SlideLayoutIdList)
register_element_cls("p:sldMaster", CT_SlideMaster)
register_element_cls("p:timing", CT_SlideTiming)
register_element_cls("p:video", CT_TLMediaNodeVideo)
from pptx.oxml.table import ( # noqa: E402
CT_Table,
CT_TableCell,
CT_TableCellProperties,
CT_TableCol,
CT_TableGrid,
CT_TableProperties,
CT_TableRow,
)
register_element_cls("a:gridCol", CT_TableCol)
register_element_cls("a:tbl", CT_Table)
register_element_cls("a:tblGrid", CT_TableGrid)
register_element_cls("a:tblPr", CT_TableProperties)
register_element_cls("a:tc", CT_TableCell)
register_element_cls("a:tcPr", CT_TableCellProperties)
register_element_cls("a:tr", CT_TableRow)
from pptx.oxml.text import ( # noqa: E402
CT_RegularTextRun,
CT_TextBody,
CT_TextBodyProperties,
CT_TextCharacterProperties,
CT_TextField,
CT_TextFont,
CT_TextLineBreak,
CT_TextNormalAutofit,
CT_TextParagraph,
CT_TextParagraphProperties,
CT_TextSpacing,
CT_TextSpacingPercent,
CT_TextSpacingPoint,
)
register_element_cls("a:bodyPr", CT_TextBodyProperties)
register_element_cls("a:br", CT_TextLineBreak)
register_element_cls("a:defRPr", CT_TextCharacterProperties)
register_element_cls("a:endParaRPr", CT_TextCharacterProperties)
register_element_cls("a:fld", CT_TextField)
register_element_cls("a:latin", CT_TextFont)
register_element_cls("a:lnSpc", CT_TextSpacing)
register_element_cls("a:normAutofit", CT_TextNormalAutofit)
register_element_cls("a:r", CT_RegularTextRun)
register_element_cls("a:p", CT_TextParagraph)
register_element_cls("a:pPr", CT_TextParagraphProperties)
register_element_cls("c:rich", CT_TextBody)
register_element_cls("a:rPr", CT_TextCharacterProperties)
register_element_cls("a:spcAft", CT_TextSpacing)
register_element_cls("a:spcBef", CT_TextSpacing)
register_element_cls("a:spcPct", CT_TextSpacingPercent)
register_element_cls("a:spcPts", CT_TextSpacingPoint)
register_element_cls("a:txBody", CT_TextBody)
register_element_cls("c:txPr", CT_TextBody)
register_element_cls("p:txBody", CT_TextBody)
from pptx.oxml.theme import CT_OfficeStyleSheet # noqa: E402
register_element_cls("a:theme", CT_OfficeStyleSheet)
@@ -0,0 +1,53 @@
"""lxml custom element classes for text-related XML elements."""
from __future__ import annotations
from pptx.oxml.simpletypes import XsdString
from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
class CT_Hyperlink(BaseOxmlElement):
"""Custom element class for <a:hlinkClick> elements."""
rId: str = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
action: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"action", XsdString
)
@property
def action_fields(self) -> dict[str, str]:
"""Query portion of the `ppaction://` URL as dict.
For example `{'id':'0', 'return':'true'}` in 'ppaction://customshow?id=0&return=true'.
Returns an empty dict if the URL contains no query string or if no action attribute is
present.
"""
url = self.action
if url is None:
return {}
halves = url.split("?")
if len(halves) == 1:
return {}
key_value_pairs = halves[1].split("&")
return dict([pair.split("=") for pair in key_value_pairs])
@property
def action_verb(self) -> str | None:
"""The host portion of the `ppaction://` URL contained in the action attribute.
For example 'customshow' in 'ppaction://customshow?id=0&return=true'. Returns |None| if no
action attribute is present.
"""
url = self.action
if url is None:
return None
protocol_and_host = url.split("?")[0]
host = protocol_and_host[11:]
return host
@@ -0,0 +1,297 @@
"""Axis-related oxml objects."""
from __future__ import annotations
from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK
from pptx.oxml.chart.shared import CT_Title
from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset, ST_Orientation
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrOne,
)
class BaseAxisElement(BaseOxmlElement):
"""Base class for catAx, dateAx, valAx, and perhaps other axis elements."""
@property
def defRPr(self):
"""
``<a:defRPr>`` great-great-grandchild element, added with its
ancestors if not present.
"""
txPr = self.get_or_add_txPr()
defRPr = txPr.defRPr
return defRPr
@property
def orientation(self):
"""Value of `val` attribute of `c:scaling/c:orientation` grandchild element.
Defaults to `ST_Orientation.MIN_MAX` if attribute or any ancestors are not
present.
"""
orientation = self.scaling.orientation
if orientation is None:
return ST_Orientation.MIN_MAX
return orientation.val
@orientation.setter
def orientation(self, value):
"""`value` is a member of `ST_Orientation`."""
self.scaling._remove_orientation()
if value == ST_Orientation.MAX_MIN:
self.scaling.get_or_add_orientation().val = value
def _new_title(self):
return CT_Title.new_title()
def _new_txPr(self):
return CT_TextBody.new_txPr()
class CT_AxisUnit(BaseOxmlElement):
"""Used for `c:majorUnit` and `c:minorUnit` elements, and others."""
val = RequiredAttribute("val", ST_AxisUnit)
class CT_CatAx(BaseAxisElement):
"""`c:catAx` element, defining a category axis."""
_tag_seq = (
"c:axId",
"c:scaling",
"c:delete",
"c:axPos",
"c:majorGridlines",
"c:minorGridlines",
"c:title",
"c:numFmt",
"c:majorTickMark",
"c:minorTickMark",
"c:tickLblPos",
"c:spPr",
"c:txPr",
"c:crossAx",
"c:crosses",
"c:crossesAt",
"c:auto",
"c:lblAlgn",
"c:lblOffset",
"c:tickLblSkip",
"c:tickMarkSkip",
"c:noMultiLvlLbl",
"c:extLst",
)
scaling = OneAndOnlyOne("c:scaling")
delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
title = ZeroOrOne("c:title", successors=_tag_seq[7:])
numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[19:])
del _tag_seq
class CT_ChartLines(BaseOxmlElement):
"""Used for `c:majorGridlines` and `c:minorGridlines`.
Specifies gridlines visual properties such as color and width.
"""
spPr = ZeroOrOne("c:spPr", successors=())
class CT_Crosses(BaseOxmlElement):
"""`c:crosses` element, specifying where the other axis crosses this one."""
val = RequiredAttribute("val", XL_AXIS_CROSSES)
class CT_DateAx(BaseAxisElement):
"""`c:dateAx` element, defining a date (category) axis."""
_tag_seq = (
"c:axId",
"c:scaling",
"c:delete",
"c:axPos",
"c:majorGridlines",
"c:minorGridlines",
"c:title",
"c:numFmt",
"c:majorTickMark",
"c:minorTickMark",
"c:tickLblPos",
"c:spPr",
"c:txPr",
"c:crossAx",
"c:crosses",
"c:crossesAt",
"c:auto",
"c:lblOffset",
"c:baseTimeUnit",
"c:majorUnit",
"c:majorTimeUnit",
"c:minorUnit",
"c:minorTimeUnit",
"c:extLst",
)
scaling = OneAndOnlyOne("c:scaling")
delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
title = ZeroOrOne("c:title", successors=_tag_seq[7:])
numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[18:])
del _tag_seq
class CT_LblOffset(BaseOxmlElement):
"""`c:lblOffset` custom element class."""
val = OptionalAttribute("val", ST_LblOffset, default=100)
class CT_Orientation(BaseOxmlElement):
"""`c:xAx/c:scaling/c:orientation` element, defining category order.
Used to reverse the order categories appear in on a bar chart so they start at the
top rather than the bottom. Because we read top-to-bottom, the default way looks odd
to many and perhaps most folks. Also applicable to value and date axes.
"""
val = OptionalAttribute("val", ST_Orientation, default=ST_Orientation.MIN_MAX)
class CT_Scaling(BaseOxmlElement):
"""`c:scaling` element.
Defines axis scale characteristics such as maximum value, log vs. linear, etc.
"""
_tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst")
orientation = ZeroOrOne("c:orientation", successors=_tag_seq[2:])
max = ZeroOrOne("c:max", successors=_tag_seq[3:])
min = ZeroOrOne("c:min", successors=_tag_seq[4:])
del _tag_seq
@property
def maximum(self):
"""
The float value of the ``<c:max>`` child element, or |None| if no max
element is present.
"""
max = self.max
if max is None:
return None
return max.val
@maximum.setter
def maximum(self, value):
"""
Set the value of the ``<c:max>`` child element to the float *value*,
or remove the max element if *value* is |None|.
"""
self._remove_max()
if value is None:
return
self._add_max(val=value)
@property
def minimum(self):
"""
The float value of the ``<c:min>`` child element, or |None| if no min
element is present.
"""
min = self.min
if min is None:
return None
return min.val
@minimum.setter
def minimum(self, value):
"""
Set the value of the ``<c:min>`` child element to the float *value*,
or remove the min element if *value* is |None|.
"""
self._remove_min()
if value is None:
return
self._add_min(val=value)
class CT_TickLblPos(BaseOxmlElement):
"""`c:tickLblPos` element."""
val = OptionalAttribute("val", XL_TICK_LABEL_POSITION)
class CT_TickMark(BaseOxmlElement):
"""Used for `c:minorTickMark` and `c:majorTickMark`."""
val = OptionalAttribute("val", XL_TICK_MARK, default=XL_TICK_MARK.CROSS)
class CT_ValAx(BaseAxisElement):
"""`c:valAx` element, defining a value axis."""
_tag_seq = (
"c:axId",
"c:scaling",
"c:delete",
"c:axPos",
"c:majorGridlines",
"c:minorGridlines",
"c:title",
"c:numFmt",
"c:majorTickMark",
"c:minorTickMark",
"c:tickLblPos",
"c:spPr",
"c:txPr",
"c:crossAx",
"c:crosses",
"c:crossesAt",
"c:crossBetween",
"c:majorUnit",
"c:minorUnit",
"c:dispUnits",
"c:extLst",
)
scaling = OneAndOnlyOne("c:scaling")
delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
title = ZeroOrOne("c:title", successors=_tag_seq[7:])
numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
crossAx = ZeroOrOne("c:crossAx", successors=_tag_seq[14:])
crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
majorUnit = ZeroOrOne("c:majorUnit", successors=_tag_seq[18:])
minorUnit = ZeroOrOne("c:minorUnit", successors=_tag_seq[19:])
del _tag_seq
@@ -0,0 +1,282 @@
"""Custom element classes for top-level chart-related XML elements."""
from __future__ import annotations
from typing import cast
from pptx.oxml import parse_xml
from pptx.oxml.chart.shared import CT_Title
from pptx.oxml.ns import nsdecls, qn
from pptx.oxml.simpletypes import ST_Style, XsdString
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
)
class CT_Chart(BaseOxmlElement):
"""`c:chart` custom element class."""
_tag_seq = (
"c:title",
"c:autoTitleDeleted",
"c:pivotFmts",
"c:view3D",
"c:floor",
"c:sideWall",
"c:backWall",
"c:plotArea",
"c:legend",
"c:plotVisOnly",
"c:dispBlanksAs",
"c:showDLblsOverMax",
"c:extLst",
)
title = ZeroOrOne("c:title", successors=_tag_seq[1:])
autoTitleDeleted = ZeroOrOne("c:autoTitleDeleted", successors=_tag_seq[2:])
plotArea = OneAndOnlyOne("c:plotArea")
legend = ZeroOrOne("c:legend", successors=_tag_seq[9:])
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
@property
def has_legend(self):
"""
True if this chart has a legend defined, False otherwise.
"""
legend = self.legend
if legend is None:
return False
return True
@has_legend.setter
def has_legend(self, bool_value):
"""
Add, remove, or leave alone the ``<c:legend>`` child element depending
on current state and *bool_value*. If *bool_value* is |True| and no
``<c:legend>`` element is present, a new default element is added.
When |False|, any existing legend element is removed.
"""
if bool(bool_value) is False:
self._remove_legend()
else:
if self.legend is None:
self._add_legend()
@staticmethod
def new_chart(rId: str) -> CT_Chart:
"""Return a new `c:chart` element."""
return cast(CT_Chart, parse_xml(f'<c:chart {nsdecls("c")} {nsdecls("r")} r:id="{rId}"/>'))
def _new_title(self):
return CT_Title.new_title()
class CT_ChartSpace(BaseOxmlElement):
"""`c:chartSpace` root element of a chart part."""
_tag_seq = (
"c:date1904",
"c:lang",
"c:roundedCorners",
"c:style",
"c:clrMapOvr",
"c:pivotSource",
"c:protection",
"c:chart",
"c:spPr",
"c:txPr",
"c:externalData",
"c:printSettings",
"c:userShapes",
"c:extLst",
)
date1904 = ZeroOrOne("c:date1904", successors=_tag_seq[1:])
style = ZeroOrOne("c:style", successors=_tag_seq[4:])
chart = OneAndOnlyOne("c:chart")
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[10:])
externalData = ZeroOrOne("c:externalData", successors=_tag_seq[11:])
del _tag_seq
@property
def catAx_lst(self):
return self.chart.plotArea.catAx_lst
@property
def date_1904(self):
"""
Return |True| if the `c:date1904` child element resolves truthy,
|False| otherwise. This value indicates whether date number values
are based on the 1900 or 1904 epoch.
"""
date1904 = self.date1904
if date1904 is None:
return False
return date1904.val
@property
def dateAx_lst(self):
return self.xpath("c:chart/c:plotArea/c:dateAx")
def get_or_add_title(self):
"""Return the `c:title` grandchild, newly created if not present."""
return self.chart.get_or_add_title()
@property
def plotArea(self):
"""
Return the required `c:chartSpace/c:chart/c:plotArea` grandchild
element.
"""
return self.chart.plotArea
@property
def valAx_lst(self):
return self.chart.plotArea.valAx_lst
@property
def xlsx_part_rId(self):
"""
The string in the required ``r:id`` attribute of the
`<c:externalData>` child, or |None| if no externalData element is
present.
"""
externalData = self.externalData
if externalData is None:
return None
return externalData.rId
def _add_externalData(self):
"""
Always add a ``<c:autoUpdate val="0"/>`` child so auto-updating
behavior is off by default.
"""
externalData = self._new_externalData()
externalData._add_autoUpdate(val=False)
self._insert_externalData(externalData)
return externalData
def _new_txPr(self):
return CT_TextBody.new_txPr()
class CT_ExternalData(BaseOxmlElement):
"""
`<c:externalData>` element, defining link to embedded Excel package part
containing the chart data.
"""
autoUpdate = ZeroOrOne("c:autoUpdate")
rId = RequiredAttribute("r:id", XsdString)
class CT_PlotArea(BaseOxmlElement):
"""
``<c:plotArea>`` element.
"""
catAx = ZeroOrMore("c:catAx")
valAx = ZeroOrMore("c:valAx")
def iter_sers(self):
"""
Generate each of the `c:ser` elements in this chart, ordered first by
the document order of the containing xChart element, then by their
ordering within the xChart element (not necessarily document order).
"""
for xChart in self.iter_xCharts():
for ser in xChart.iter_sers():
yield ser
def iter_xCharts(self):
"""
Generate each xChart child element in document.
"""
plot_tags = (
qn("c:area3DChart"),
qn("c:areaChart"),
qn("c:bar3DChart"),
qn("c:barChart"),
qn("c:bubbleChart"),
qn("c:doughnutChart"),
qn("c:line3DChart"),
qn("c:lineChart"),
qn("c:ofPieChart"),
qn("c:pie3DChart"),
qn("c:pieChart"),
qn("c:radarChart"),
qn("c:scatterChart"),
qn("c:stockChart"),
qn("c:surface3DChart"),
qn("c:surfaceChart"),
)
for child in self.iterchildren():
if child.tag not in plot_tags:
continue
yield child
@property
def last_ser(self):
"""
Return the last `<c:ser>` element in the last xChart element, based
on series order (not necessarily the same element as document order).
"""
last_xChart = self.xCharts[-1]
sers = last_xChart.sers
if not sers:
return None
return sers[-1]
@property
def next_idx(self):
"""
Return the next available `c:ser/c:idx` value within the scope of
this chart, the maximum idx value found on existing series,
incremented by one.
"""
idx_vals = [s.idx.val for s in self.sers]
if not idx_vals:
return 0
return max(idx_vals) + 1
@property
def next_order(self):
"""
Return the next available `c:ser/c:order` value within the scope of
this chart, the maximum order value found on existing series,
incremented by one.
"""
order_vals = [s.order.val for s in self.sers]
if not order_vals:
return 0
return max(order_vals) + 1
@property
def sers(self):
"""
Return a sequence containing all the `c:ser` elements in this chart,
ordered first by the document order of the containing xChart element,
then by their ordering within the xChart element (not necessarily
document order).
"""
return tuple(self.iter_sers())
@property
def xCharts(self):
"""
Return a sequence containing all the `c:{x}Chart` elements in this
chart, in document order.
"""
return tuple(self.iter_xCharts())
class CT_Style(BaseOxmlElement):
"""
``<c:style>`` element; defines the chart style.
"""
val = RequiredAttribute("val", ST_Style)
@@ -0,0 +1,252 @@
"""Chart data-label related oxml objects."""
from __future__ import annotations
from pptx.enum.chart import XL_DATA_LABEL_POSITION
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
)
class CT_DLbl(BaseOxmlElement):
"""
``<c:dLbl>`` element specifying the properties of the data label for an
individual data point.
"""
_tag_seq = (
"c:idx",
"c:layout",
"c:tx",
"c:numFmt",
"c:spPr",
"c:txPr",
"c:dLblPos",
"c:showLegendKey",
"c:showVal",
"c:showCatName",
"c:showSerName",
"c:showPercent",
"c:showBubbleSize",
"c:separator",
"c:extLst",
)
idx = OneAndOnlyOne("c:idx")
tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[5:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[7:])
del _tag_seq
def get_or_add_rich(self):
"""
Return the `c:rich` descendant representing the text frame of the
data label, newly created if not present. Any existing `c:strRef`
element is removed along with its contents.
"""
tx = self.get_or_add_tx()
tx._remove_strRef()
return tx.get_or_add_rich()
def get_or_add_tx_rich(self):
"""
Return the `c:tx[c:rich]` subtree, newly created if not present.
"""
tx = self.get_or_add_tx()
tx._remove_strRef()
tx.get_or_add_rich()
return tx
@property
def idx_val(self):
"""
The integer value of the `val` attribute on the required `c:idx`
child.
"""
return self.idx.val
@classmethod
def new_dLbl(cls):
"""Return a newly created "loose" `c:dLbl` element.
The `c:dLbl` element contains the same (fairly extensive) default
subtree added by PowerPoint when an individual data label is
customized in the UI. Note that the idx value must be set by the
client. Failure to set the idx value will likely result in any
changes not being visible and may result in a repair error on open.
"""
return parse_xml(
"<c:dLbl %s>\n"
' <c:idx val="666"/>\n'
" <c:spPr/>\n"
" <c:txPr>\n"
" <a:bodyPr/>\n"
" <a:lstStyle/>\n"
" <a:p>\n"
" <a:pPr>\n"
" <a:defRPr/>\n"
" </a:pPr>\n"
" </a:p>\n"
" </c:txPr>\n"
' <c:showLegendKey val="0"/>\n'
' <c:showVal val="1"/>\n'
' <c:showCatName val="0"/>\n'
' <c:showSerName val="0"/>\n'
' <c:showPercent val="0"/>\n'
' <c:showBubbleSize val="0"/>\n'
"</c:dLbl>" % nsdecls("c", "a")
)
def remove_tx_rich(self):
"""
Remove any `c:tx[c:rich]` child, or do nothing if not present.
"""
matches = self.xpath("c:tx[c:rich]")
if not matches:
return
tx = matches[0]
self.remove(tx)
def _new_txPr(self):
return CT_TextBody.new_txPr()
class CT_DLblPos(BaseOxmlElement):
"""
``<c:dLblPos>`` element specifying the positioning of a data label with
respect to its data point.
"""
val = RequiredAttribute("val", XL_DATA_LABEL_POSITION)
class CT_DLbls(BaseOxmlElement):
"""`c:dLbls` element specifying properties for a set of data labels."""
_tag_seq = (
"c:dLbl",
"c:numFmt",
"c:spPr",
"c:txPr",
"c:dLblPos",
"c:showLegendKey",
"c:showVal",
"c:showCatName",
"c:showSerName",
"c:showPercent",
"c:showBubbleSize",
"c:separator",
"c:showLeaderLines",
"c:leaderLines",
"c:extLst",
)
dLbl = ZeroOrMore("c:dLbl", successors=_tag_seq[1:])
numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[2:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[4:])
dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[5:])
showLegendKey = ZeroOrOne("c:showLegendKey", successors=_tag_seq[6:])
showVal = ZeroOrOne("c:showVal", successors=_tag_seq[7:])
showCatName = ZeroOrOne("c:showCatName", successors=_tag_seq[8:])
showSerName = ZeroOrOne("c:showSerName", successors=_tag_seq[9:])
showPercent = ZeroOrOne("c:showPercent", successors=_tag_seq[10:])
del _tag_seq
@property
def defRPr(self):
"""
``<a:defRPr>`` great-great-grandchild element, added with its
ancestors if not present.
"""
txPr = self.get_or_add_txPr()
defRPr = txPr.defRPr
return defRPr
def get_dLbl_for_point(self, idx):
"""
Return the `c:dLbl` child representing the label for the data point
at index *idx*.
"""
matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
if matches:
return matches[0]
return None
def get_or_add_dLbl_for_point(self, idx):
"""
Return the `c:dLbl` element representing the label of the point at
index *idx*.
"""
matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
if matches:
return matches[0]
return self._insert_dLbl_in_sequence(idx)
@classmethod
def new_dLbls(cls):
"""Return a newly created "loose" `c:dLbls` element."""
return parse_xml(
"<c:dLbls %s>\n"
' <c:showLegendKey val="0"/>\n'
' <c:showVal val="0"/>\n'
' <c:showCatName val="0"/>\n'
' <c:showSerName val="0"/>\n'
' <c:showPercent val="0"/>\n'
' <c:showBubbleSize val="0"/>\n'
' <c:showLeaderLines val="1"/>\n'
"</c:dLbls>" % nsdecls("c")
)
def _insert_dLbl_in_sequence(self, idx):
"""
Return a newly created `c:dLbl` element having `c:idx` child of *idx*
and inserted in numeric sequence among the `c:dLbl` children of this
element.
"""
new_dLbl = self._new_dLbl()
new_dLbl.idx.val = idx
dLbl = None
for dLbl in self.dLbl_lst:
if dLbl.idx_val > idx:
dLbl.addprevious(new_dLbl)
return new_dLbl
if dLbl is not None:
dLbl.addnext(new_dLbl)
else:
self.insert(0, new_dLbl)
return new_dLbl
def _new_dLbl(self):
return CT_DLbl.new_dLbl()
def _new_showCatName(self):
"""Return a new `c:showCatName` with value initialized.
This method is called by the metaclass-generated code whenever a new
`c:showCatName` element is required. In this case, it defaults to
`val=true`, which is not what we need so we override to make val
explicitly False.
"""
return parse_xml('<c:showCatName %s val="0"/>' % nsdecls("c"))
def _new_showLegendKey(self):
return parse_xml('<c:showLegendKey %s val="0"/>' % nsdecls("c"))
def _new_showPercent(self):
return parse_xml('<c:showPercent %s val="0"/>' % nsdecls("c"))
def _new_showSerName(self):
return parse_xml('<c:showSerName %s val="0"/>' % nsdecls("c"))
def _new_showVal(self):
return parse_xml('<c:showVal %s val="0"/>' % nsdecls("c"))
def _new_txPr(self):
return CT_TextBody.new_txPr()
@@ -0,0 +1,72 @@
"""lxml custom element classes for legend-related XML elements."""
from __future__ import annotations
from pptx.enum.chart import XL_LEGEND_POSITION
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne
class CT_Legend(BaseOxmlElement):
"""
``<c:legend>`` custom element class
"""
_tag_seq = (
"c:legendPos",
"c:legendEntry",
"c:layout",
"c:overlay",
"c:spPr",
"c:txPr",
"c:extLst",
)
legendPos = ZeroOrOne("c:legendPos", successors=_tag_seq[1:])
layout = ZeroOrOne("c:layout", successors=_tag_seq[3:])
overlay = ZeroOrOne("c:overlay", successors=_tag_seq[4:])
txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
del _tag_seq
@property
def defRPr(self):
"""
`./c:txPr/a:p/a:pPr/a:defRPr` great-great-grandchild element, added
with its ancestors if not present.
"""
txPr = self.get_or_add_txPr()
defRPr = txPr.defRPr
return defRPr
@property
def horz_offset(self):
"""
The float value in ./c:layout/c:manualLayout/c:x when
./c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that
XPath expression has no match.
"""
layout = self.layout
if layout is None:
return 0.0
return layout.horz_offset
@horz_offset.setter
def horz_offset(self, offset):
"""
Set the value of ./c:layout/c:manualLayout/c:x@val to *offset* and
./c:layout/c:manualLayout/c:xMode@val to "factor". Remove
./c:layout/c:manualLayout if *offset* == 0.
"""
layout = self.get_or_add_layout()
layout.horz_offset = offset
def _new_txPr(self):
return CT_TextBody.new_txPr()
class CT_LegendPos(BaseOxmlElement):
"""
``<c:legendPos>`` element specifying position of legend with respect to
chart as a member of ST_LegendPos.
"""
val = OptionalAttribute("val", XL_LEGEND_POSITION, default=XL_LEGEND_POSITION.RIGHT)
@@ -0,0 +1,61 @@
"""Series-related oxml objects."""
from __future__ import annotations
from pptx.enum.chart import XL_MARKER_STYLE
from pptx.oxml.simpletypes import ST_MarkerSize
from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne
class CT_Marker(BaseOxmlElement):
"""
`c:marker` custom element class, containing visual properties for a data
point marker on line-type charts.
"""
_tag_seq = ("c:symbol", "c:size", "c:spPr", "c:extLst")
symbol = ZeroOrOne("c:symbol", successors=_tag_seq[1:])
size = ZeroOrOne("c:size", successors=_tag_seq[2:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[3:])
del _tag_seq
@property
def size_val(self):
"""
Return the value of `./c:size/@val`, specifying the size of this
marker in points. Returns |None| if no `c:size` element is present or
its val attribute is not present.
"""
size = self.size
if size is None:
return None
return size.val
@property
def symbol_val(self):
"""
Return the value of `./c:symbol/@val`, specifying the shape of this
marker. Returns |None| if no `c:symbol` element is present.
"""
symbol = self.symbol
if symbol is None:
return None
return symbol.val
class CT_MarkerSize(BaseOxmlElement):
"""
`c:size` custom element class, specifying the size (in points) of a data
point marker for a line, XY, or radar chart.
"""
val = RequiredAttribute("val", ST_MarkerSize)
class CT_MarkerStyle(BaseOxmlElement):
"""
`c:symbol` custom element class, specifying the shape of a data point
marker for a line, XY, or radar chart.
"""
val = RequiredAttribute("val", XL_MARKER_STYLE)
@@ -0,0 +1,345 @@
"""Plot-related oxml objects."""
from __future__ import annotations
from pptx.oxml.chart.datalabel import CT_DLbls
from pptx.oxml.simpletypes import (
ST_BarDir,
ST_BubbleScale,
ST_GapAmount,
ST_Grouping,
ST_Overlap,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
OptionalAttribute,
ZeroOrMore,
ZeroOrOne,
)
class BaseChartElement(BaseOxmlElement):
"""
Base class for barChart, lineChart, and other plot elements.
"""
@property
def cat(self):
"""
Return the `c:cat` element of the first series in this xChart, or
|None| if not present.
"""
cats = self.xpath("./c:ser[1]/c:cat")
return cats[0] if cats else None
@property
def cat_pt_count(self):
"""
Return the value of the `c:ptCount` descendent of this xChart
element. Its parent can be one of three element types. This value
represents the true number of (leaf) categories, although they might
not all have a corresponding `c:pt` sibling; a category with no label
does not get a `c:pt` element. Returns 0 if there is no `c:ptCount`
descendent.
"""
cat_ptCounts = self.xpath("./c:ser//c:cat//c:ptCount")
if not cat_ptCounts:
return 0
return cat_ptCounts[0].val
@property
def cat_pts(self):
"""
Return a sequence representing the `c:pt` elements under the `c:cat`
element of the first series in this xChart element. A category having
no value will have no corresponding `c:pt` element; |None| will
appear in that position in such cases. Items appear in `idx` order.
Only those in the first ``<c:lvl>`` element are included in the case
of multi-level categories.
"""
cat_pts = self.xpath("./c:ser[1]/c:cat//c:lvl[1]/c:pt")
if not cat_pts:
cat_pts = self.xpath("./c:ser[1]/c:cat//c:pt")
cat_pt_dict = dict((pt.idx, pt) for pt in cat_pts)
return [cat_pt_dict.get(idx, None) for idx in range(self.cat_pt_count)]
@property
def grouping_val(self):
"""
Return the value of the ``./c:grouping{val=?}`` attribute, taking
defaults into account when items are not present.
"""
grouping = self.grouping
if grouping is None:
return ST_Grouping.STANDARD
val = grouping.val
if val is None:
return ST_Grouping.STANDARD
return val
def iter_sers(self):
"""
Generate each ``<c:ser>`` child element in this xChart in
c:order/@val sequence (not document or c:idx order).
"""
def ser_order(ser):
return ser.order.val
return (ser for ser in sorted(self.xpath("./c:ser"), key=ser_order))
@property
def sers(self):
"""
Sequence of ``<c:ser>`` child elements in this xChart in c:order/@val
sequence (not document or c:idx order).
"""
return tuple(self.iter_sers())
def _new_dLbls(self):
return CT_DLbls.new_dLbls()
class CT_Area3DChart(BaseChartElement):
"""
``<c:area3DChart>`` element.
"""
grouping = ZeroOrOne(
"c:grouping",
successors=(
"c:varyColors",
"c:ser",
"c:dLbls",
"c:dropLines",
"c:gapDepth",
"c:axId",
),
)
class CT_AreaChart(BaseChartElement):
"""
``<c:areaChart>`` element.
"""
_tag_seq = (
"c:grouping",
"c:varyColors",
"c:ser",
"c:dLbls",
"c:dropLines",
"c:axId",
"c:extLst",
)
grouping = ZeroOrOne("c:grouping", successors=_tag_seq[1:])
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[4:])
del _tag_seq
class CT_BarChart(BaseChartElement):
"""
``<c:barChart>`` element.
"""
_tag_seq = (
"c:barDir",
"c:grouping",
"c:varyColors",
"c:ser",
"c:dLbls",
"c:gapWidth",
"c:overlap",
"c:serLines",
"c:axId",
"c:extLst",
)
barDir = OneAndOnlyOne("c:barDir")
grouping = ZeroOrOne("c:grouping", successors=_tag_seq[2:])
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[3:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[4:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[5:])
gapWidth = ZeroOrOne("c:gapWidth", successors=_tag_seq[6:])
overlap = ZeroOrOne("c:overlap", successors=_tag_seq[7:])
del _tag_seq
@property
def grouping_val(self):
"""
Return the value of the ``./c:grouping{val=?}`` attribute, taking
defaults into account when items are not present.
"""
grouping = self.grouping
if grouping is None:
return ST_Grouping.CLUSTERED
val = grouping.val
if val is None:
return ST_Grouping.CLUSTERED
return val
class CT_BarDir(BaseOxmlElement):
"""
``<c:barDir>`` child of a barChart element, specifying the orientation of
the bars, 'bar' if they are horizontal and 'col' if they are vertical.
"""
val = OptionalAttribute("val", ST_BarDir, default=ST_BarDir.COL)
class CT_BubbleChart(BaseChartElement):
"""
``<c:bubbleChart>`` custom element class
"""
_tag_seq = (
"c:varyColors",
"c:ser",
"c:dLbls",
"c:axId",
"c:bubble3D",
"c:bubbleScale",
"c:showNegBubbles",
"c:sizeRepresents",
"c:axId",
"c:extLst",
)
ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
bubble3D = ZeroOrOne("c:bubble3D", successors=_tag_seq[5:])
bubbleScale = ZeroOrOne("c:bubbleScale", successors=_tag_seq[6:])
del _tag_seq
class CT_BubbleScale(BaseChartElement):
"""
``<c:bubbleScale>`` custom element class
"""
val = OptionalAttribute("val", ST_BubbleScale, default=100)
class CT_DoughnutChart(BaseChartElement):
"""
``<c:doughnutChart>`` element.
"""
_tag_seq = (
"c:varyColors",
"c:ser",
"c:dLbls",
"c:firstSliceAng",
"c:holeSize",
"c:extLst",
)
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
del _tag_seq
class CT_GapAmount(BaseOxmlElement):
"""
``<c:gapWidth>`` child of ``<c:barChart>`` element, also used for other
purposes like error bars.
"""
val = OptionalAttribute("val", ST_GapAmount, default=150)
class CT_Grouping(BaseOxmlElement):
"""
``<c:grouping>`` child of an xChart element, specifying a value like
'clustered' or 'stacked'. Also used for variants with the same tag name
like CT_BarGrouping.
"""
val = OptionalAttribute("val", ST_Grouping)
class CT_LineChart(BaseChartElement):
"""
``<c:lineChart>`` custom element class
"""
_tag_seq = (
"c:grouping",
"c:varyColors",
"c:ser",
"c:dLbls",
"c:dropLines",
"c:hiLowLines",
"c:upDownBars",
"c:marker",
"c:smooth",
"c:axId",
"c:extLst",
)
grouping = ZeroOrOne("c:grouping", successors=(_tag_seq[1:]))
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
del _tag_seq
class CT_Overlap(BaseOxmlElement):
"""
``<c:overlap>`` element specifying bar overlap as an integer percentage
of bar width, in range -100 to 100.
"""
val = OptionalAttribute("val", ST_Overlap, default=0)
class CT_PieChart(BaseChartElement):
"""
``<c:pieChart>`` custom element class
"""
_tag_seq = ("c:varyColors", "c:ser", "c:dLbls", "c:firstSliceAng", "c:extLst")
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
del _tag_seq
class CT_RadarChart(BaseChartElement):
"""
``<c:radarChart>`` custom element class
"""
_tag_seq = (
"c:radarStyle",
"c:varyColors",
"c:ser",
"c:dLbls",
"c:axId",
"c:extLst",
)
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
del _tag_seq
class CT_ScatterChart(BaseChartElement):
"""
``<c:scatterChart>`` custom element class
"""
_tag_seq = (
"c:scatterStyle",
"c:varyColors",
"c:ser",
"c:dLbls",
"c:axId",
"c:extLst",
)
varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
del _tag_seq
@@ -0,0 +1,254 @@
"""Series-related oxml objects."""
from __future__ import annotations
from pptx.oxml.chart.datalabel import CT_DLbls
from pptx.oxml.simpletypes import XsdUnsignedInt
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OneAndOnlyOne,
OxmlElement,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
)
class CT_AxDataSource(BaseOxmlElement):
"""
``<c:cat>`` custom element class used in category charts to specify
category labels and hierarchy.
"""
multiLvlStrRef = ZeroOrOne("c:multiLvlStrRef", successors=())
@property
def lvls(self):
"""
Return a list containing the `c:lvl` descendent elements in document
order. These will only be present when the required single child
is a `c:multiLvlStrRef` element. Returns an empty list when no
`c:lvl` descendent elements are present.
"""
return self.xpath(".//c:lvl")
class CT_DPt(BaseOxmlElement):
"""
``<c:dPt>`` custom element class, containing visual properties for a data
point.
"""
_tag_seq = (
"c:idx",
"c:invertIfNegative",
"c:marker",
"c:bubble3D",
"c:explosion",
"c:spPr",
"c:pictureOptions",
"c:extLst",
)
idx = OneAndOnlyOne("c:idx")
marker = ZeroOrOne("c:marker", successors=_tag_seq[3:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[6:])
del _tag_seq
@classmethod
def new_dPt(cls):
"""
Return a newly created "loose" `c:dPt` element containing its default
subtree.
"""
dPt = OxmlElement("c:dPt")
dPt.append(OxmlElement("c:idx"))
return dPt
class CT_Lvl(BaseOxmlElement):
"""
``<c:lvl>`` custom element class used in multi-level categories to
specify a level of hierarchy.
"""
pt = ZeroOrMore("c:pt", successors=())
class CT_NumDataSource(BaseOxmlElement):
"""
``<c:yVal>`` custom element class used in XY and bubble charts, and
perhaps others.
"""
numRef = OneAndOnlyOne("c:numRef")
@property
def ptCount_val(self):
"""
Return the value of `./c:numRef/c:numCache/c:ptCount/@val`,
specifying how many `c:pt` elements are in this numeric data cache.
Returns 0 if no `c:ptCount` element is present, as this is the least
disruptive way to degrade when no cached point data is available.
This situation is not expected, but is valid according to the schema.
"""
results = self.xpath(".//c:ptCount/@val")
return int(results[0]) if results else 0
def pt_v(self, idx):
"""
Return the Y value for data point *idx* in this cache, or None if no
value is present for that data point.
"""
results = self.xpath(".//c:pt[@idx=%d]" % idx)
return results[0].value if results else None
class CT_SeriesComposite(BaseOxmlElement):
"""
``<c:ser>`` custom element class. Note there are several different series
element types in the schema, such as ``CT_LineSer`` and ``CT_BarSer``,
but they all share the same tag name. This class acts as a composite and
depends on the caller not to do anything invalid for a series belonging
to a particular plot type.
"""
_tag_seq = (
"c:idx",
"c:order",
"c:tx",
"c:spPr",
"c:invertIfNegative",
"c:pictureOptions",
"c:marker",
"c:explosion",
"c:dPt",
"c:dLbls",
"c:trendline",
"c:errBars",
"c:cat",
"c:val",
"c:xVal",
"c:yVal",
"c:shape",
"c:smooth",
"c:bubbleSize",
"c:bubble3D",
"c:extLst",
)
idx = OneAndOnlyOne("c:idx")
order = OneAndOnlyOne("c:order")
tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
invertIfNegative = ZeroOrOne("c:invertIfNegative", successors=_tag_seq[5:])
marker = ZeroOrOne("c:marker", successors=_tag_seq[7:])
dPt = ZeroOrMore("c:dPt", successors=_tag_seq[9:])
dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[10:])
cat = ZeroOrOne("c:cat", successors=_tag_seq[13:])
val = ZeroOrOne("c:val", successors=_tag_seq[14:])
xVal = ZeroOrOne("c:xVal", successors=_tag_seq[15:])
yVal = ZeroOrOne("c:yVal", successors=_tag_seq[16:])
smooth = ZeroOrOne("c:smooth", successors=_tag_seq[18:])
bubbleSize = ZeroOrOne("c:bubbleSize", successors=_tag_seq[19:])
del _tag_seq
@property
def bubbleSize_ptCount_val(self):
"""
Return the number of bubble size values as reflected in the `val`
attribute of `./c:bubbleSize//c:ptCount`, or 0 if not present.
"""
vals = self.xpath("./c:bubbleSize//c:ptCount/@val")
if not vals:
return 0
return int(vals[0])
@property
def cat_ptCount_val(self):
"""
Return the number of categories as reflected in the `val` attribute
of `./c:cat//c:ptCount`, or 0 if not present.
"""
vals = self.xpath("./c:cat//c:ptCount/@val")
if not vals:
return 0
return int(vals[0])
def get_dLbl(self, idx):
"""
Return the `c:dLbl` element representing the label for the data point
at offset *idx* in this series, or |None| if not present.
"""
dLbls = self.dLbls
if dLbls is None:
return None
return dLbls.get_dLbl_for_point(idx)
def get_or_add_dLbl(self, idx):
"""
Return the `c:dLbl` element representing the label of the point at
offset *idx* in this series, newly created if not yet present.
"""
dLbls = self.get_or_add_dLbls()
return dLbls.get_or_add_dLbl_for_point(idx)
def get_or_add_dPt_for_point(self, idx):
"""
Return the `c:dPt` child representing the visual properties of the
data point at index *idx*.
"""
matches = self.xpath('c:dPt[c:idx[@val="%d"]]' % idx)
if matches:
return matches[0]
dPt = self._add_dPt()
dPt.idx.val = idx
return dPt
@property
def xVal_ptCount_val(self):
"""
Return the number of X values as reflected in the `val` attribute of
`./c:xVal//c:ptCount`, or 0 if not present.
"""
vals = self.xpath("./c:xVal//c:ptCount/@val")
if not vals:
return 0
return int(vals[0])
@property
def yVal_ptCount_val(self):
"""
Return the number of Y values as reflected in the `val` attribute of
`./c:yVal//c:ptCount`, or 0 if not present.
"""
vals = self.xpath("./c:yVal//c:ptCount/@val")
if not vals:
return 0
return int(vals[0])
def _new_dLbls(self):
"""Override metaclass method that creates `c:dLbls` element."""
return CT_DLbls.new_dLbls()
def _new_dPt(self):
"""
Overrides the metaclass generated method to get `c:dPt` with minimal
subtree.
"""
return CT_DPt.new_dPt()
class CT_StrVal_NumVal_Composite(BaseOxmlElement):
"""
``<c:pt>`` element, can be either CT_StrVal or CT_NumVal complex type.
Using this class for both, differentiating as needed.
"""
v = OneAndOnlyOne("c:v")
idx = RequiredAttribute("idx", XsdUnsignedInt)
@property
def value(self):
"""
The float value of the text in the required ``<c:v>`` child.
"""
return float(self.v.text)
@@ -0,0 +1,219 @@
"""Shared oxml objects for charts."""
from __future__ import annotations
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import (
ST_LayoutMode,
XsdBoolean,
XsdDouble,
XsdString,
XsdUnsignedInt,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
OptionalAttribute,
RequiredAttribute,
ZeroOrOne,
)
class CT_Boolean(BaseOxmlElement):
"""
Common complex type used for elements having a True/False value.
"""
val = OptionalAttribute("val", XsdBoolean, default=True)
class CT_Boolean_Explicit(BaseOxmlElement):
"""Always spells out the `val` attribute, e.g. `val=1`.
At least one boolean element is improperly interpreted by one or more
versions of PowerPoint. The `c:overlay` element is interpreted as |False|
when no `val` attribute is present, contrary to the behavior described in
the schema. A remedy for this is to interpret a missing `val` attribute
as |True| (consistent with the spec), but always write the attribute
whenever there is occasion for changing the element.
"""
_val = OptionalAttribute("val", XsdBoolean, default=True)
@property
def val(self):
return self._val
@val.setter
def val(self, value):
val_str = "1" if bool(value) is True else "0"
self.set("val", val_str)
class CT_Double(BaseOxmlElement):
"""
Used for floating point values.
"""
val = RequiredAttribute("val", XsdDouble)
class CT_Layout(BaseOxmlElement):
"""
``<c:layout>`` custom element class
"""
manualLayout = ZeroOrOne("c:manualLayout", successors=("c:extLst",))
@property
def horz_offset(self):
"""
The float value in ./c:manualLayout/c:x when
c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that XPath
expression finds no match.
"""
manualLayout = self.manualLayout
if manualLayout is None:
return 0.0
return manualLayout.horz_offset
@horz_offset.setter
def horz_offset(self, offset):
"""
Set the value of ./c:manualLayout/c:x@val to *offset* and
./c:manualLayout/c:xMode@val to "factor". Remove ./c:manualLayout if
*offset* == 0.
"""
if offset == 0.0:
self._remove_manualLayout()
return
manualLayout = self.get_or_add_manualLayout()
manualLayout.horz_offset = offset
class CT_LayoutMode(BaseOxmlElement):
"""
Used for ``<c:xMode>``, ``<c:yMode>``, ``<c:wMode>``, and ``<c:hMode>``
child elements of CT_ManualLayout.
"""
val = OptionalAttribute("val", ST_LayoutMode, default=ST_LayoutMode.FACTOR)
class CT_ManualLayout(BaseOxmlElement):
"""
``<c:manualLayout>`` custom element class
"""
_tag_seq = (
"c:layoutTarget",
"c:xMode",
"c:yMode",
"c:wMode",
"c:hMode",
"c:x",
"c:y",
"c:w",
"c:h",
"c:extLst",
)
xMode = ZeroOrOne("c:xMode", successors=_tag_seq[2:])
x = ZeroOrOne("c:x", successors=_tag_seq[6:])
del _tag_seq
@property
def horz_offset(self):
"""
The float value in ./c:x@val when ./c:xMode@val == "factor". 0.0 when
./c:x is not present or ./c:xMode@val != "factor".
"""
x, xMode = self.x, self.xMode
if x is None or xMode is None or xMode.val != ST_LayoutMode.FACTOR:
return 0.0
return x.val
@horz_offset.setter
def horz_offset(self, offset):
"""
Set the value of ./c:x@val to *offset* and ./c:xMode@val to "factor".
"""
self.get_or_add_xMode().val = ST_LayoutMode.FACTOR
self.get_or_add_x().val = offset
class CT_NumFmt(BaseOxmlElement):
"""
``<c:numFmt>`` element specifying the formatting for number labels on a
tick mark or data point.
"""
formatCode = RequiredAttribute("formatCode", XsdString)
sourceLinked = OptionalAttribute("sourceLinked", XsdBoolean)
class CT_Title(BaseOxmlElement):
"""`c:title` custom element class."""
_tag_seq = ("c:tx", "c:layout", "c:overlay", "c:spPr", "c:txPr", "c:extLst")
tx = ZeroOrOne("c:tx", successors=_tag_seq[1:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
del _tag_seq
def get_or_add_tx_rich(self):
"""Return `c:tx/c:rich`, newly created if not present.
Return the `c:rich` grandchild at `c:tx/c:rich`. Both the `c:tx` and
`c:rich` elements are created if not already present. Any
`c:tx/c:strRef` element is removed. (Such an element would contain
a cell reference for the axis title text in the chart's Excel
worksheet.)
"""
tx = self.get_or_add_tx()
tx._remove_strRef()
return tx.get_or_add_rich()
@property
def tx_rich(self):
"""Return `c:tx/c:rich` or |None| if not present."""
richs = self.xpath("c:tx/c:rich")
if not richs:
return None
return richs[0]
@staticmethod
def new_title():
"""Return "loose" `c:title` element containing default children."""
return parse_xml(
"<c:title %s>" " <c:layout/>" ' <c:overlay val="0"/>' "</c:title>" % nsdecls("c")
)
class CT_Tx(BaseOxmlElement):
"""
``<c:tx>`` element containing the text for a label on a data point or
other chart item.
"""
strRef = ZeroOrOne("c:strRef")
rich = ZeroOrOne("c:rich")
def _new_rich(self):
return parse_xml(
"<c:rich %s>"
" <a:bodyPr/>"
" <a:lstStyle/>"
" <a:p>"
" <a:pPr>"
" <a:defRPr/>"
" </a:pPr>"
" </a:p>"
"</c:rich>" % nsdecls("c", "a")
)
class CT_UnsignedInt(BaseOxmlElement):
"""
``<c:idx>`` element and others.
"""
val = RequiredAttribute("val", XsdUnsignedInt)
@@ -0,0 +1,288 @@
"""lxml custom element classes for core properties-related XML elements."""
from __future__ import annotations
import datetime as dt
import re
from typing import Callable, cast
from lxml.etree import _Element # pyright: ignore[reportPrivateUsage]
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls, qn
from pptx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
class CT_CoreProperties(BaseOxmlElement):
"""`cp:coreProperties` element.
The root element of the Core Properties part stored as `/docProps/core.xml`. Implements many
of the Dublin Core document metadata elements. String elements resolve to an empty string ('')
if the element is not present in the XML. String elements are limited in length to 255 unicode
characters.
"""
get_or_add_revision: Callable[[], _Element]
category = ZeroOrOne("cp:category", successors=())
contentStatus = ZeroOrOne("cp:contentStatus", successors=())
created = ZeroOrOne("dcterms:created", successors=())
creator = ZeroOrOne("dc:creator", successors=())
description = ZeroOrOne("dc:description", successors=())
identifier = ZeroOrOne("dc:identifier", successors=())
keywords = ZeroOrOne("cp:keywords", successors=())
language = ZeroOrOne("dc:language", successors=())
lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=())
lastPrinted = ZeroOrOne("cp:lastPrinted", successors=())
modified = ZeroOrOne("dcterms:modified", successors=())
revision: _Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"cp:revision", successors=()
)
subject = ZeroOrOne("dc:subject", successors=())
title = ZeroOrOne("dc:title", successors=())
version = ZeroOrOne("cp:version", successors=())
_coreProperties_tmpl = "<cp:coreProperties %s/>\n" % nsdecls("cp", "dc", "dcterms")
@staticmethod
def new_coreProperties() -> CT_CoreProperties:
"""Return a new `cp:coreProperties` element"""
return cast(CT_CoreProperties, parse_xml(CT_CoreProperties._coreProperties_tmpl))
@property
def author_text(self) -> str:
return self._text_of_element("creator")
@author_text.setter
def author_text(self, value: str):
self._set_element_text("creator", value)
@property
def category_text(self) -> str:
return self._text_of_element("category")
@category_text.setter
def category_text(self, value: str):
self._set_element_text("category", value)
@property
def comments_text(self) -> str:
return self._text_of_element("description")
@comments_text.setter
def comments_text(self, value: str):
self._set_element_text("description", value)
@property
def contentStatus_text(self) -> str:
return self._text_of_element("contentStatus")
@contentStatus_text.setter
def contentStatus_text(self, value: str):
self._set_element_text("contentStatus", value)
@property
def created_datetime(self):
return self._datetime_of_element("created")
@created_datetime.setter
def created_datetime(self, value: dt.datetime):
self._set_element_datetime("created", value)
@property
def identifier_text(self) -> str:
return self._text_of_element("identifier")
@identifier_text.setter
def identifier_text(self, value: str):
self._set_element_text("identifier", value)
@property
def keywords_text(self) -> str:
return self._text_of_element("keywords")
@keywords_text.setter
def keywords_text(self, value: str):
self._set_element_text("keywords", value)
@property
def language_text(self) -> str:
return self._text_of_element("language")
@language_text.setter
def language_text(self, value: str):
self._set_element_text("language", value)
@property
def lastModifiedBy_text(self) -> str:
return self._text_of_element("lastModifiedBy")
@lastModifiedBy_text.setter
def lastModifiedBy_text(self, value: str):
self._set_element_text("lastModifiedBy", value)
@property
def lastPrinted_datetime(self):
return self._datetime_of_element("lastPrinted")
@lastPrinted_datetime.setter
def lastPrinted_datetime(self, value: dt.datetime):
self._set_element_datetime("lastPrinted", value)
@property
def modified_datetime(self):
return self._datetime_of_element("modified")
@modified_datetime.setter
def modified_datetime(self, value: dt.datetime):
self._set_element_datetime("modified", value)
@property
def revision_number(self) -> int:
"""Integer value of revision property."""
revision = self.revision
if revision is None:
return 0
revision_str = revision.text
if revision_str is None:
return 0
try:
revision = int(revision_str)
except ValueError:
# -- non-integer revision strings also resolve to 0 --
return 0
# -- as do negative integers --
if revision < 0:
return 0
return revision
@revision_number.setter
def revision_number(self, value: int):
"""Set revision property to string value of integer `value`."""
if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance]
tmpl = "revision property requires positive int, got '%s'"
raise ValueError(tmpl % value)
revision = self.get_or_add_revision()
revision.text = str(value)
@property
def subject_text(self) -> str:
return self._text_of_element("subject")
@subject_text.setter
def subject_text(self, value: str):
self._set_element_text("subject", value)
@property
def title_text(self) -> str:
return self._text_of_element("title")
@title_text.setter
def title_text(self, value: str):
self._set_element_text("title", value)
@property
def version_text(self) -> str:
return self._text_of_element("version")
@version_text.setter
def version_text(self, value: str):
self._set_element_text("version", value)
def _datetime_of_element(self, property_name: str) -> dt.datetime | None:
element = cast("_Element | None", getattr(self, property_name))
if element is None:
return None
datetime_str = element.text
if datetime_str is None:
return None
try:
return self._parse_W3CDTF_to_datetime(datetime_str)
except ValueError:
# invalid datetime strings are ignored
return None
def _get_or_add(self, prop_name: str):
"""Return element returned by 'get_or_add_' method for `prop_name`."""
get_or_add_method_name = "get_or_add_%s" % prop_name
get_or_add_method = getattr(self, get_or_add_method_name)
element = get_or_add_method()
return element
@classmethod
def _offset_dt(cls, datetime: dt.datetime, offset_str: str):
"""Return |datetime| instance offset from `datetime` by offset specified in `offset_str`.
`offset_str` is a string like `'-07:00'`.
"""
match = cls._offset_pattern.match(offset_str)
if match is None:
raise ValueError(f"{repr(offset_str)} is not a valid offset string")
sign, hours_str, minutes_str = match.groups()
sign_factor = -1 if sign == "+" else 1
hours = int(hours_str) * sign_factor
minutes = int(minutes_str) * sign_factor
td = dt.timedelta(hours=hours, minutes=minutes)
return datetime + td
_offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)")
@classmethod
def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime:
# valid W3CDTF date cases:
# yyyy e.g. '2003'
# yyyy-mm e.g. '2003-12'
# yyyy-mm-dd e.g. '2003-12-31'
# UTC timezone e.g. '2003-12-31T10:14:55Z'
# numeric timezone e.g. '2003-12-31T10:14:55-08:00'
templates = ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y-%m", "%Y")
# strptime isn't smart enough to parse literal timezone offsets like
# '-07:30', so we have to do it ourselves
parseable_part = w3cdtf_str[:19]
offset_str = w3cdtf_str[19:]
timestamp = None
for tmpl in templates:
try:
timestamp = dt.datetime.strptime(parseable_part, tmpl)
except ValueError:
continue
if timestamp is None:
tmpl = "could not parse W3CDTF datetime string '%s'"
raise ValueError(tmpl % w3cdtf_str)
if len(offset_str) == 6:
return cls._offset_dt(timestamp, offset_str)
return timestamp
def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None:
"""Set date/time value of child element having `prop_name` to `value`."""
if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance]
tmpl = "property requires <type 'datetime.datetime'> object, got %s"
raise ValueError(tmpl % type(value))
element = self._get_or_add(prop_name)
dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ")
element.text = dt_str
if prop_name in ("created", "modified"):
# These two require an explicit 'xsi:type="dcterms:W3CDTF"'
# attribute. The first and last line are a hack required to add
# the xsi namespace to the root element rather than each child
# element in which it is referenced
self.set(qn("xsi:foo"), "bar")
element.set(qn("xsi:type"), "dcterms:W3CDTF")
del self.attrib[qn("xsi:foo")]
def _set_element_text(self, prop_name: str, value: str) -> None:
"""Set string value of `name` property to `value`."""
value = str(value)
if len(value) > 255:
tmpl = "exceeded 255 char limit for property, got:\n\n'%s'"
raise ValueError(tmpl % value)
element = self._get_or_add(prop_name)
element.text = value
def _text_of_element(self, property_name: str) -> str:
element = getattr(self, property_name)
if element is None:
return ""
if element.text is None:
return ""
return element.text
@@ -0,0 +1,111 @@
"""lxml custom element classes for DrawingML-related XML elements."""
from __future__ import annotations
from pptx.enum.dml import MSO_THEME_COLOR
from pptx.oxml.simpletypes import ST_HexColorRGB, ST_Percentage
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
RequiredAttribute,
ZeroOrOne,
ZeroOrOneChoice,
)
class _BaseColorElement(BaseOxmlElement):
"""
Base class for <a:srgbClr> and <a:schemeClr> elements.
"""
lumMod = ZeroOrOne("a:lumMod")
lumOff = ZeroOrOne("a:lumOff")
def add_lumMod(self, value):
"""
Return a newly added <a:lumMod> child element.
"""
lumMod = self._add_lumMod()
lumMod.val = value
return lumMod
def add_lumOff(self, value):
"""
Return a newly added <a:lumOff> child element.
"""
lumOff = self._add_lumOff()
lumOff.val = value
return lumOff
def clear_lum(self):
"""
Return self after removing any <a:lumMod> and <a:lumOff> child
elements.
"""
self._remove_lumMod()
self._remove_lumOff()
return self
class CT_Color(BaseOxmlElement):
"""Custom element class for `a:fgClr`, `a:bgClr` and perhaps others."""
eg_colorChoice = ZeroOrOneChoice(
(
Choice("a:scrgbClr"),
Choice("a:srgbClr"),
Choice("a:hslClr"),
Choice("a:sysClr"),
Choice("a:schemeClr"),
Choice("a:prstClr"),
),
successors=(),
)
class CT_HslColor(_BaseColorElement):
"""
Custom element class for <a:hslClr> element.
"""
class CT_Percentage(BaseOxmlElement):
"""
Custom element class for <a:lumMod> and <a:lumOff> elements.
"""
val = RequiredAttribute("val", ST_Percentage)
class CT_PresetColor(_BaseColorElement):
"""
Custom element class for <a:prstClr> element.
"""
class CT_SchemeColor(_BaseColorElement):
"""
Custom element class for <a:schemeClr> element.
"""
val = RequiredAttribute("val", MSO_THEME_COLOR)
class CT_ScRgbColor(_BaseColorElement):
"""
Custom element class for <a:scrgbClr> element.
"""
class CT_SRgbColor(_BaseColorElement):
"""
Custom element class for <a:srgbClr> element.
"""
val = RequiredAttribute("val", ST_HexColorRGB)
class CT_SystemColor(_BaseColorElement):
"""
Custom element class for <a:sysClr> element.
"""
@@ -0,0 +1,197 @@
"""lxml custom element classes for DrawingML-related XML elements."""
from __future__ import annotations
from pptx.enum.dml import MSO_PATTERN_TYPE
from pptx.oxml import parse_xml
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import (
ST_Percentage,
ST_PositiveFixedAngle,
ST_PositiveFixedPercentage,
ST_RelationshipId,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OneOrMore,
OptionalAttribute,
RequiredAttribute,
ZeroOrOne,
ZeroOrOneChoice,
)
class CT_Blip(BaseOxmlElement):
"""
<a:blip> element
"""
rEmbed = OptionalAttribute("r:embed", ST_RelationshipId)
class CT_BlipFillProperties(BaseOxmlElement):
"""
Custom element class for <a:blipFill> element.
"""
_tag_seq = ("a:blip", "a:srcRect", "a:tile", "a:stretch")
blip = ZeroOrOne("a:blip", successors=_tag_seq[1:])
srcRect = ZeroOrOne("a:srcRect", successors=_tag_seq[2:])
del _tag_seq
def crop(self, cropping):
"""
Set `a:srcRect` child to crop according to *cropping* values.
"""
srcRect = self._add_srcRect()
srcRect.l, srcRect.t, srcRect.r, srcRect.b = cropping
class CT_GradientFillProperties(BaseOxmlElement):
"""`a:gradFill` custom element class."""
_tag_seq = ("a:gsLst", "a:lin", "a:path", "a:tileRect")
gsLst = ZeroOrOne("a:gsLst", successors=_tag_seq[1:])
lin = ZeroOrOne("a:lin", successors=_tag_seq[2:])
path = ZeroOrOne("a:path", successors=_tag_seq[3:])
del _tag_seq
@classmethod
def new_gradFill(cls):
"""Return newly-created "loose" default gradient subtree."""
return parse_xml(
'<a:gradFill %s rotWithShape="1">\n'
" <a:gsLst>\n"
' <a:gs pos="0">\n'
' <a:schemeClr val="accent1">\n'
' <a:tint val="100000"/>\n'
' <a:shade val="100000"/>\n'
' <a:satMod val="130000"/>\n'
" </a:schemeClr>\n"
" </a:gs>\n"
' <a:gs pos="100000">\n'
' <a:schemeClr val="accent1">\n'
' <a:tint val="50000"/>\n'
' <a:shade val="100000"/>\n'
' <a:satMod val="350000"/>\n'
" </a:schemeClr>\n"
" </a:gs>\n"
" </a:gsLst>\n"
' <a:lin scaled="0"/>\n'
"</a:gradFill>\n" % nsdecls("a")
)
def _new_gsLst(self):
"""Override default to add minimum subtree."""
return CT_GradientStopList.new_gsLst()
class CT_GradientStop(BaseOxmlElement):
"""`a:gs` custom element class."""
eg_colorChoice = ZeroOrOneChoice(
(
Choice("a:scrgbClr"),
Choice("a:srgbClr"),
Choice("a:hslClr"),
Choice("a:sysClr"),
Choice("a:schemeClr"),
Choice("a:prstClr"),
),
successors=(),
)
pos = RequiredAttribute("pos", ST_PositiveFixedPercentage)
class CT_GradientStopList(BaseOxmlElement):
"""`a:gsLst` custom element class."""
gs = OneOrMore("a:gs")
@classmethod
def new_gsLst(cls):
"""Return newly-created "loose" default stop-list subtree.
An `a:gsLst` element must have at least two `a:gs` children. These
are the default from the PowerPoint built-in "White" template.
"""
return parse_xml(
"<a:gsLst %s>\n"
' <a:gs pos="0">\n'
' <a:schemeClr val="accent1">\n'
' <a:tint val="100000"/>\n'
' <a:shade val="100000"/>\n'
' <a:satMod val="130000"/>\n'
" </a:schemeClr>\n"
" </a:gs>\n"
' <a:gs pos="100000">\n'
' <a:schemeClr val="accent1">\n'
' <a:tint val="50000"/>\n'
' <a:shade val="100000"/>\n'
' <a:satMod val="350000"/>\n'
" </a:schemeClr>\n"
" </a:gs>\n"
"</a:gsLst>\n" % nsdecls("a")
)
class CT_GroupFillProperties(BaseOxmlElement):
"""`a:grpFill` custom element class"""
class CT_LinearShadeProperties(BaseOxmlElement):
"""`a:lin` custom element class"""
ang = OptionalAttribute("ang", ST_PositiveFixedAngle)
class CT_NoFillProperties(BaseOxmlElement):
"""`a:noFill` custom element class"""
class CT_PatternFillProperties(BaseOxmlElement):
"""`a:pattFill` custom element class"""
_tag_seq = ("a:fgClr", "a:bgClr")
fgClr = ZeroOrOne("a:fgClr", successors=_tag_seq[1:])
bgClr = ZeroOrOne("a:bgClr", successors=_tag_seq[2:])
del _tag_seq
prst = OptionalAttribute("prst", MSO_PATTERN_TYPE)
def _new_bgClr(self):
"""Override default to add minimum subtree."""
xml = ("<a:bgClr %s>\n" ' <a:srgbClr val="FFFFFF"/>\n' "</a:bgClr>\n") % nsdecls("a")
bgClr = parse_xml(xml)
return bgClr
def _new_fgClr(self):
"""Override default to add minimum subtree."""
xml = ("<a:fgClr %s>\n" ' <a:srgbClr val="000000"/>\n' "</a:fgClr>\n") % nsdecls("a")
fgClr = parse_xml(xml)
return fgClr
class CT_RelativeRect(BaseOxmlElement):
"""`a:srcRect` element and perhaps others."""
l = OptionalAttribute("l", ST_Percentage, default=0.0) # noqa
t = OptionalAttribute("t", ST_Percentage, default=0.0)
r = OptionalAttribute("r", ST_Percentage, default=0.0)
b = OptionalAttribute("b", ST_Percentage, default=0.0)
class CT_SolidColorFillProperties(BaseOxmlElement):
"""`a:solidFill` custom element class."""
eg_colorChoice = ZeroOrOneChoice(
(
Choice("a:scrgbClr"),
Choice("a:srgbClr"),
Choice("a:hslClr"),
Choice("a:sysClr"),
Choice("a:schemeClr"),
Choice("a:prstClr"),
),
successors=(),
)
@@ -0,0 +1,12 @@
"""lxml custom element classes for DrawingML line-related XML elements."""
from __future__ import annotations
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
class CT_PresetLineDashProperties(BaseOxmlElement):
"""`a:prstDash` custom element class"""
val = OptionalAttribute("val", MSO_LINE_DASH_STYLE)
@@ -0,0 +1,129 @@
"""Namespace related objects."""
from __future__ import annotations
# -- Maps namespace prefix to namespace name for all known PowerPoint XML namespaces --
_nsmap = {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"c": "http://schemas.openxmlformats.org/drawingml/2006/chart",
"cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
"ct": "http://schemas.openxmlformats.org/package/2006/content-types",
"dc": "http://purl.org/dc/elements/1.1/",
"dcmitype": "http://purl.org/dc/dcmitype/",
"dcterms": "http://purl.org/dc/terms/",
"ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
"i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
"m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
"mo": "http://schemas.microsoft.com/office/mac/office/2008/main",
"mv": "urn:schemas-microsoft-com:mac:vml",
"o": "urn:schemas-microsoft-com:office:office",
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing",
"pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
"pr": "http://schemas.openxmlformats.org/package/2006/relationships",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
"v": "urn:schemas-microsoft-com:vml",
"ve": "http://schemas.openxmlformats.org/markup-compatibility/2006",
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"w10": "urn:schemas-microsoft-com:office:word",
"wne": "http://schemas.microsoft.com/office/word/2006/wordml",
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
pfxmap = {value: key for key, value in _nsmap.items()}
class NamespacePrefixedTag(str):
"""Value object that knows the semantics of an XML tag having a namespace prefix."""
def __new__(cls, nstag: str):
return super(NamespacePrefixedTag, cls).__new__(cls, nstag)
def __init__(self, nstag: str):
self._pfx, self._local_part = nstag.split(":")
self._ns_uri = _nsmap[self._pfx]
@classmethod
def from_clark_name(cls, clark_name: str) -> NamespacePrefixedTag:
nsuri, local_name = clark_name[1:].split("}")
nstag = "%s:%s" % (pfxmap[nsuri], local_name)
return cls(nstag)
@property
def clark_name(self):
return "{%s}%s" % (self._ns_uri, self._local_part)
@property
def local_part(self):
"""
Return the local part of the tag as a string. E.g. 'foobar' is
returned for tag 'f:foobar'.
"""
return self._local_part
@property
def nsmap(self):
"""
Return a dict having a single member, mapping the namespace prefix of
this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This
is handy for passing to xpath calls and other uses.
"""
return {self._pfx: self._ns_uri}
@property
def nspfx(self):
"""
Return the string namespace prefix for the tag, e.g. 'f' is returned
for tag 'f:foobar'.
"""
return self._pfx
@property
def nsuri(self):
"""
Return the namespace URI for the tag, e.g. 'http://foo/bar' would be
returned for tag 'f:foobar' if the 'f' prefix maps to
'http://foo/bar' in _nsmap.
"""
return self._ns_uri
def namespaces(*prefixes: str):
"""Return a dict containing the subset namespace prefix mappings specified by *prefixes*.
Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p').
"""
return {pfx: _nsmap[pfx] for pfx in prefixes}
nsmap = namespaces # alias for more compact use with Element()
def nsdecls(*prefixes: str):
return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes])
def nsuri(nspfx: str):
"""Return the namespace URI corresponding to `nspfx`.
Example:
>>> nsuri("p")
"http://schemas.openxmlformats.org/presentationml/2006/main"
"""
return _nsmap[nspfx]
def qn(namespace_prefixed_tag: str) -> str:
"""Return a Clark-notation qualified tag name corresponding to `namespace_prefixed_tag`.
`namespace_prefixed_tag` is a string like 'p:body'. 'qn' stands for `qualified name`.
As an example, `qn("p:cSld")` returns:
`"{http://schemas.openxmlformats.org/drawingml/2006/main}cSld"`.
"""
nsptag = NamespacePrefixedTag(namespace_prefixed_tag)
return nsptag.clark_name
@@ -0,0 +1,130 @@
"""Custom element classes for presentation-related XML elements."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, cast
from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString
from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne
if TYPE_CHECKING:
from pptx.util import Length
class CT_Presentation(BaseOxmlElement):
"""`p:presentation` element, root of the Presentation part stored as `/ppt/presentation.xml`."""
get_or_add_sldSz: Callable[[], CT_SlideSize]
get_or_add_sldIdLst: Callable[[], CT_SlideIdList]
get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList]
sldMasterIdLst: CT_SlideMasterIdList | None = (
ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldMasterIdLst",
successors=(
"p:notesMasterIdLst",
"p:handoutMasterIdLst",
"p:sldIdLst",
"p:sldSz",
"p:notesSz",
),
)
)
sldIdLst: CT_SlideIdList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldIdLst", successors=("p:sldSz", "p:notesSz")
)
sldSz: CT_SlideSize | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldSz", successors=("p:notesSz",)
)
class CT_SlideId(BaseOxmlElement):
"""`p:sldId` element.
Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation.
"""
id: int = RequiredAttribute("id", ST_SlideId) # pyright: ignore[reportAssignmentType]
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
class CT_SlideIdList(BaseOxmlElement):
"""`p:sldIdLst` element.
Direct child of <p:presentation> that contains a list of the slide parts in the presentation.
"""
sldId_lst: list[CT_SlideId]
_add_sldId: Callable[..., CT_SlideId]
sldId = ZeroOrMore("p:sldId")
def add_sldId(self, rId: str) -> CT_SlideId:
"""Create and return a reference to a new `p:sldId` child element.
The new `p:sldId` element has its r:id attribute set to `rId`.
"""
return self._add_sldId(id=self._next_id, rId=rId)
@property
def _next_id(self) -> int:
"""The next available slide ID as an `int`.
Valid slide IDs start at 256. The next integer value greater than the max value in use is
chosen, which minimizes that chance of reusing the id of a deleted slide.
"""
MIN_SLIDE_ID = 256
MAX_SLIDE_ID = 2147483647
used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldId/@id"))]
simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1
if simple_next <= MAX_SLIDE_ID:
return simple_next
# -- fall back to search for next unused from bottom --
valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID))
return (
next(
candidate_id
for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID)
if candidate_id != used_id
)
if valid_used_ids
else 256
)
class CT_SlideMasterIdList(BaseOxmlElement):
"""`p:sldMasterIdLst` element.
Child of `p:presentation` containing references to the slide masters that belong to the
presentation.
"""
sldMasterId_lst: list[CT_SlideMasterIdListEntry]
sldMasterId = ZeroOrMore("p:sldMasterId")
class CT_SlideMasterIdListEntry(BaseOxmlElement):
"""
``<p:sldMasterId>`` element, child of ``<p:sldMasterIdLst>`` containing
a reference to a slide master.
"""
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
class CT_SlideSize(BaseOxmlElement):
"""`p:sldSz` element.
Direct child of <p:presentation> that contains the width and height of slides in the
presentation.
"""
cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"cx", ST_SlideSizeCoordinate
)
cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"cy", ST_SlideSizeCoordinate
)
@@ -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
@@ -0,0 +1,740 @@
"""Simple-type classes.
A "simple-type" is a scalar type, generally serving as an XML attribute. This is in contrast to a
"complex-type" which would specify an XML element.
These objects providing validation and format translation for values stored in XML element
attributes. Naming generally corresponds to the simple type in the associated XML schema.
"""
from __future__ import annotations
import numbers
from typing import Any
from pptx.exc import InvalidXmlError
from pptx.util import Centipoints, Emu
class BaseSimpleType:
@classmethod
def from_xml(cls, xml_value: str) -> Any:
return cls.convert_from_xml(xml_value)
@classmethod
def to_xml(cls, value: Any) -> str:
cls.validate(value)
str_value = cls.convert_to_xml(value)
return str_value
@classmethod
def validate_float(cls, value: Any):
"""Note that int values are accepted."""
if not isinstance(value, (int, float)):
raise TypeError("value must be a number, got %s" % type(value))
@classmethod
def validate_int(cls, value):
if not isinstance(value, numbers.Integral):
raise TypeError("value must be an integral type, got %s" % type(value))
@classmethod
def validate_float_in_range(cls, value, min_inclusive, max_inclusive):
cls.validate_float(value)
if value < min_inclusive or value > max_inclusive:
raise ValueError(
"value must be in range %s to %s inclusive, got %s"
% (min_inclusive, max_inclusive, value)
)
@classmethod
def validate_int_in_range(cls, value, min_inclusive, max_inclusive):
cls.validate_int(value)
if value < min_inclusive or value > max_inclusive:
raise ValueError(
"value must be in range %d to %d inclusive, got %d"
% (min_inclusive, max_inclusive, value)
)
@classmethod
def validate_string(cls, value):
if isinstance(value, str):
return value
try:
if isinstance(value, basestring):
return value
except NameError: # means we're on Python 3
pass
raise TypeError("value must be a string, got %s" % type(value))
class BaseFloatType(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value):
return float(str_value)
@classmethod
def convert_to_xml(cls, value):
return str(float(value))
@classmethod
def validate(cls, value):
if not isinstance(value, (int, float)):
raise TypeError("value must be a number, got %s" % type(value))
class BaseIntType(BaseSimpleType):
@classmethod
def convert_from_percent_literal(cls, str_value):
int_str = str_value.replace("%", "")
return int(int_str)
@classmethod
def convert_from_xml(cls, str_value):
return int(str_value)
@classmethod
def convert_to_xml(cls, value):
return str(value)
@classmethod
def validate(cls, value):
cls.validate_int(value)
class BaseStringType(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value):
return str_value
@classmethod
def convert_to_xml(cls, value):
return value
@classmethod
def validate(cls, value):
cls.validate_string(value)
class BaseStringEnumerationType(BaseStringType):
@classmethod
def validate(cls, value):
cls.validate_string(value)
if value not in cls._members:
raise ValueError("must be one of %s, got '%s'" % (cls._members, value))
class XsdAnyUri(BaseStringType):
"""
There's a regular expression this is supposed to meet but so far thinking
spending cycles on validating wouldn't be worth it for the number of
programming errors it would catch.
"""
class XsdBoolean(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value):
if str_value not in ("1", "0", "true", "false"):
raise InvalidXmlError(
"value must be one of '1', '0', 'true' or 'false', got '%s'" % str_value
)
return str_value in ("1", "true")
@classmethod
def convert_to_xml(cls, value):
return {True: "1", False: "0"}[value]
@classmethod
def validate(cls, value):
if value not in (True, False):
raise TypeError(
"only True or False (and possibly None) may be assigned, got" " '%s'" % value
)
class XsdDouble(BaseFloatType):
pass
class XsdId(BaseStringType):
"""
String that must begin with a letter or underscore and cannot contain any
colons. Not fully validated because not used in external API.
"""
class XsdInt(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, -2147483648, 2147483647)
class XsdLong(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, -9223372036854775808, 9223372036854775807)
class XsdString(BaseStringType):
pass
class XsdStringEnumeration(BaseStringEnumerationType):
"""
Set of enumerated xsd:string values.
"""
class XsdToken(BaseStringType):
"""
xsd:string with whitespace collapsing, e.g. multiple spaces reduced to
one, leading and trailing space stripped.
"""
class XsdTokenEnumeration(BaseStringEnumerationType):
"""
xsd:string with whitespace collapsing, e.g. multiple spaces reduced to
one, leading and trailing space stripped.
"""
class XsdUnsignedByte(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 255)
class XsdUnsignedInt(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 4294967295)
class XsdUnsignedShort(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 65535)
class ST_Angle(XsdInt):
"""
Valid values for `rot` attribute on `<a:xfrm>` element. 60000ths of
a degree rotation.
"""
DEGREE_INCREMENTS = 60000
THREE_SIXTY = 360 * DEGREE_INCREMENTS
@classmethod
def convert_from_xml(cls, str_value: str) -> float:
rot = int(str_value) % cls.THREE_SIXTY
return float(rot) / cls.DEGREE_INCREMENTS
@classmethod
def convert_to_xml(cls, value):
"""
Convert signed angle float like -42.42 to int 60000 per degree,
normalized to positive value.
"""
# modulo normalizes negative and >360 degree values
rot = int(round(value * cls.DEGREE_INCREMENTS)) % cls.THREE_SIXTY
return str(rot)
@classmethod
def validate(cls, value):
BaseFloatType.validate(value)
class ST_AxisUnit(XsdDouble):
"""
Valid values for val attribute on c:majorUnit and others.
"""
@classmethod
def validate(cls, value):
super(ST_AxisUnit, cls).validate(value)
if value <= 0.0:
raise ValueError("must be positive numeric value, got %s" % value)
class ST_BarDir(XsdStringEnumeration):
"""
Valid values for <c:barDir val="?"> attribute
"""
BAR = "bar"
COL = "col"
_members = (BAR, COL)
class ST_BubbleScale(BaseIntType):
"""
String value is an integer in range 0-300, representing a percent,
optionally including a '%' suffix.
"""
@classmethod
def convert_from_xml(cls, str_value):
if "%" in str_value:
return cls.convert_from_percent_literal(str_value)
return super(ST_BubbleScale, cls).convert_from_xml(str_value)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 300)
class ST_ContentType(XsdString):
"""
Has a pretty wicked regular expression it needs to match in the schema,
but figuring it's not worth the trouble or run time to identify
a programming error (as opposed to a user/runtime error).
"""
pass
class ST_Coordinate(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value):
if "i" in str_value or "m" in str_value or "p" in str_value:
return ST_UniversalMeasure.convert_from_xml(str_value)
return Emu(int(str_value))
@classmethod
def convert_to_xml(cls, value):
return str(value)
@classmethod
def validate(cls, value):
ST_CoordinateUnqualified.validate(value)
class ST_Coordinate32(BaseSimpleType):
"""
xsd:union of ST_Coordinate32Unqualified, ST_UniversalMeasure
"""
@classmethod
def convert_from_xml(cls, str_value):
if "i" in str_value or "m" in str_value or "p" in str_value:
return ST_UniversalMeasure.convert_from_xml(str_value)
return ST_Coordinate32Unqualified.convert_from_xml(str_value)
@classmethod
def convert_to_xml(cls, value):
return ST_Coordinate32Unqualified.convert_to_xml(value)
@classmethod
def validate(cls, value):
ST_Coordinate32Unqualified.validate(value)
class ST_Coordinate32Unqualified(XsdInt):
@classmethod
def convert_from_xml(cls, str_value):
return Emu(int(str_value))
class ST_CoordinateUnqualified(XsdLong):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, -27273042329600, 27273042316900)
class ST_Direction(XsdTokenEnumeration):
"""Valid values for `<p:ph orient="...">` attribute."""
HORZ = "horz"
VERT = "vert"
_members = (HORZ, VERT)
class ST_DrawingElementId(XsdUnsignedInt):
pass
class ST_Extension(XsdString):
"""
Has a regular expression it needs to match in the schema, but figuring
it's not worth the trouble or run time to identify a programming error
(as opposed to a user/runtime error).
"""
pass
class ST_GapAmount(BaseIntType):
"""
String value is an integer in range 0-500, representing a percent,
optionally including a '%' suffix.
"""
@classmethod
def convert_from_xml(cls, str_value):
if "%" in str_value:
return cls.convert_from_percent_literal(str_value)
return super(ST_GapAmount, cls).convert_from_xml(str_value)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 500)
class ST_Grouping(XsdStringEnumeration):
"""
Valid values for <c:grouping val=""> attribute. Overloaded for use as
ST_BarGrouping using same tag name.
"""
CLUSTERED = "clustered"
PERCENT_STACKED = "percentStacked"
STACKED = "stacked"
STANDARD = "standard"
_members = (CLUSTERED, PERCENT_STACKED, STACKED, STANDARD)
class ST_HexColorRGB(BaseStringType):
@classmethod
def convert_to_xml(cls, value):
"""
Keep alpha characters all uppercase just for consistency.
"""
return value.upper()
@classmethod
def validate(cls, value):
# must be string ---------------
str_value = cls.validate_string(value)
# must be 6 chars long----------
if len(str_value) != 6:
raise ValueError("RGB string must be six characters long, got '%s'" % str_value)
# must parse as hex int --------
try:
int(str_value, 16)
except ValueError:
raise ValueError("RGB string must be valid hex string, got '%s'" % str_value)
class ST_LayoutMode(XsdStringEnumeration):
"""
Valid values for `val` attribute on c:xMode and other elements of type
CT_LayoutMode.
"""
EDGE = "edge"
FACTOR = "factor"
_members = (EDGE, FACTOR)
class ST_LblOffset(XsdUnsignedShort):
"""
Unsigned integer value between 0 and 1000 inclusive, with optional
percent character ('%') suffix.
"""
@classmethod
def convert_from_xml(cls, str_value):
if str_value.endswith("%"):
return cls.convert_from_percent_literal(str_value)
return int(str_value)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 1000)
class ST_LineWidth(XsdInt):
@classmethod
def convert_from_xml(cls, str_value):
return Emu(int(str_value))
@classmethod
def validate(cls, value):
super(ST_LineWidth, cls).validate(value)
if value < 0 or value > 20116800:
raise ValueError(
"value must be in range 0-20116800 inclusive (0-1584 points)" ", got %d" % value
)
class ST_MarkerSize(XsdUnsignedByte):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 2, 72)
class ST_Orientation(XsdStringEnumeration):
"""Valid values for `val` attribute on c:orientation (CT_Orientation)."""
MAX_MIN = "maxMin"
MIN_MAX = "minMax"
_members = (MAX_MIN, MIN_MAX)
class ST_Overlap(BaseIntType):
"""
String value is an integer in range -100..100, representing a percent,
optionally including a '%' suffix.
"""
@classmethod
def convert_from_xml(cls, str_value):
if "%" in str_value:
return cls.convert_from_percent_literal(str_value)
return super(ST_Overlap, cls).convert_from_xml(str_value)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, -100, 100)
class ST_Percentage(BaseIntType):
"""Percentage value like 42000 or '42.0%'
Either an integer literal representing 1000ths of a percent
(e.g. "42000"), or a floating point literal with a '%' suffix
(e.g. "42.0%).
"""
@classmethod
def convert_from_xml(cls, str_value):
if "%" in str_value:
return cls._convert_from_percent_literal(str_value)
return int(str_value) / 100000.0
@classmethod
def convert_to_xml(cls, value):
return str(int(round(value * 100000.0)))
@classmethod
def validate(cls, value):
cls.validate_float_in_range(value, -21474.83648, 21474.83647)
@classmethod
def _convert_from_percent_literal(cls, str_value):
float_part = str_value[:-1] # trim off '%' character
return float(float_part) / 100.0
class ST_PlaceholderSize(XsdTokenEnumeration):
"""
Valid values for <p:ph> sz (size) attribute
"""
FULL = "full"
HALF = "half"
QUARTER = "quarter"
_members = (FULL, HALF, QUARTER)
class ST_PositiveCoordinate(XsdLong):
@classmethod
def convert_from_xml(cls, str_value):
int_value = super(ST_PositiveCoordinate, cls).convert_from_xml(str_value)
return Emu(int_value)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 27273042316900)
class ST_PositiveFixedAngle(ST_Angle):
"""Valid values for `a:lin@ang`.
60000ths of a degree rotation, constained to positive angles less than
360 degrees.
"""
@classmethod
def convert_to_xml(cls, degrees):
"""Convert signed angle float like -427.42 to int 60000 per degree.
Value is normalized to a positive value less than 360 degrees.
"""
if degrees < 0.0:
degrees %= -360
degrees += 360
elif degrees > 0.0:
degrees %= 360
return str(int(round(degrees * cls.DEGREE_INCREMENTS)))
class ST_PositiveFixedPercentage(ST_Percentage):
"""Percentage value between 0 and 100% like 42000 or '42.0%'
Either an integer literal representing 1000ths of a percent
(e.g. "42000"), or a floating point literal with a '%' suffix
(e.g. "42.0%). Value is constrained to range of 0% to 100%. The source
value is a float between 0.0 and 1.0.
"""
@classmethod
def validate(cls, value):
cls.validate_float_in_range(value, 0.0, 1.0)
class ST_RelationshipId(XsdString):
pass
class ST_SlideId(XsdUnsignedInt):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 256, 2147483647)
class ST_SlideSizeCoordinate(BaseIntType):
@classmethod
def convert_from_xml(cls, str_value):
return Emu(str_value)
@classmethod
def validate(cls, value):
cls.validate_int(value)
if value < 914400 or value > 51206400:
raise ValueError(
"value must be in range(914400, 51206400) (1-56 inches), got" " %d" % value
)
class ST_Style(XsdUnsignedByte):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 1, 48)
class ST_TargetMode(XsdString):
"""
The valid values for the ``TargetMode`` attribute in a Relationship
element, either 'External' or 'Internal'.
"""
@classmethod
def validate(cls, value):
cls.validate_string(value)
if value not in ("External", "Internal"):
raise ValueError("must be one of 'Internal' or 'External', got '%s'" % value)
class ST_TextFontScalePercentOrPercentString(BaseFloatType):
"""
Valid values for the `fontScale` attribute of ``<a:normAutofit>``.
Translates to a float value.
"""
@classmethod
def convert_from_xml(cls, str_value):
if str_value.endswith("%"):
return float(str_value[:-1]) # trim off '%' character
return int(str_value) / 1000.0
@classmethod
def convert_to_xml(cls, value):
return str(int(value * 1000.0))
@classmethod
def validate(cls, value):
BaseFloatType.validate(value)
if value < 1.0 or value > 100.0:
raise ValueError("value must be in range 1.0..100.0 (percent), got %s" % value)
class ST_TextFontSize(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 100, 400000)
class ST_TextIndentLevelType(BaseIntType):
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 8)
class ST_TextSpacingPercentOrPercentString(BaseFloatType):
@classmethod
def convert_from_xml(cls, str_value):
if str_value.endswith("%"):
return cls._convert_from_percent_literal(str_value)
return int(str_value) / 100000.0
@classmethod
def _convert_from_percent_literal(cls, str_value):
float_part = str_value[:-1] # trim off '%' character
percent_value = float(float_part)
lines_value = percent_value / 100.0
return lines_value
@classmethod
def convert_to_xml(cls, value):
"""
1.75 -> '175000'
"""
lines = value * 100000.0
return str(int(round(lines)))
@classmethod
def validate(cls, value):
cls.validate_float_in_range(value, 0.0, 132.0)
class ST_TextSpacingPoint(BaseIntType):
@classmethod
def convert_from_xml(cls, str_value):
"""
Reads string integer centipoints, returns |Length| value.
"""
return Centipoints(int(str_value))
@classmethod
def convert_to_xml(cls, value):
length = Emu(value) # just to make sure
return str(length.centipoints)
@classmethod
def validate(cls, value):
cls.validate_int_in_range(value, 0, 20116800)
class ST_TextTypeface(XsdString):
pass
class ST_TextWrappingType(XsdTokenEnumeration):
"""
Valid values for <a:bodyPr wrap=""> attribute
"""
NONE = "none"
SQUARE = "square"
_members = (NONE, SQUARE)
class ST_UniversalMeasure(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value):
float_part, units_part = str_value[:-2], str_value[-2:]
quantity = float(float_part)
multiplier = {
"mm": 36000,
"cm": 360000,
"in": 914400,
"pt": 12700,
"pc": 152400,
"pi": 152400,
}[units_part]
emu_value = Emu(int(round(quantity * multiplier)))
return emu_value
@@ -0,0 +1,347 @@
"""Slide-related custom element classes, including those for masters."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, cast
from pptx.oxml import parse_from_template, parse_xml
from pptx.oxml.dml.fill import CT_GradientFillProperties
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import XsdString
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
ZeroOrOneChoice,
)
if TYPE_CHECKING:
from pptx.oxml.shapes.groupshape import CT_GroupShape
class _BaseSlideElement(BaseOxmlElement):
"""Base class for the six slide types, providing common methods."""
cSld: CT_CommonSlideData
@property
def spTree(self) -> CT_GroupShape:
"""Return required `p:cSld/p:spTree` grandchild."""
return self.cSld.spTree
class CT_Background(BaseOxmlElement):
"""`p:bg` element."""
_insert_bgPr: Callable[[CT_BackgroundProperties], None]
# ---these two are actually a choice, not a sequence, but simpler for
# ---present purposes this way.
_tag_seq = ("p:bgPr", "p:bgRef")
bgPr: CT_BackgroundProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:bgPr", successors=()
)
bgRef = ZeroOrOne("p:bgRef", successors=())
del _tag_seq
def add_noFill_bgPr(self):
"""Return a new `p:bgPr` element with noFill properties."""
xml = "<p:bgPr %s>\n" " <a:noFill/>\n" " <a:effectLst/>\n" "</p:bgPr>" % nsdecls("a", "p")
bgPr = cast(CT_BackgroundProperties, parse_xml(xml))
self._insert_bgPr(bgPr)
return bgPr
class CT_BackgroundProperties(BaseOxmlElement):
"""`p:bgPr` element."""
_tag_seq = (
"a:noFill",
"a:solidFill",
"a:gradFill",
"a:blipFill",
"a:pattFill",
"a:grpFill",
"a:effectLst",
"a:effectDag",
"a:extLst",
)
eg_fillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:blipFill"),
Choice("a:pattFill"),
Choice("a:grpFill"),
),
successors=_tag_seq[6:],
)
del _tag_seq
def _new_gradFill(self):
"""Override default to add default gradient subtree."""
return CT_GradientFillProperties.new_gradFill()
class CT_CommonSlideData(BaseOxmlElement):
"""`p:cSld` element."""
_remove_bg: Callable[[], None]
get_or_add_bg: Callable[[], CT_Background]
_tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst")
bg: CT_Background | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:bg", successors=_tag_seq[1:]
)
spTree: CT_GroupShape = OneAndOnlyOne("p:spTree") # pyright: ignore[reportAssignmentType]
del _tag_seq
name: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"name", XsdString, default=""
)
def get_or_add_bgPr(self) -> CT_BackgroundProperties:
"""Return `p:bg/p:bgPr` grandchild.
If no such grandchild is present, any existing `p:bg` child is first removed and a new
default `p:bg` with noFill settings is added.
"""
bg = self.bg
if bg is None or bg.bgPr is None:
bg = self._change_to_noFill_bg()
return cast(CT_BackgroundProperties, bg.bgPr)
def _change_to_noFill_bg(self) -> CT_Background:
"""Establish a `p:bg` child with no-fill settings.
Any existing `p:bg` child is first removed.
"""
self._remove_bg()
bg = self.get_or_add_bg()
bg.add_noFill_bgPr()
return bg
class CT_NotesMaster(_BaseSlideElement):
"""`p:notesMaster` element, root of a notes master part."""
_tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
@classmethod
def new_default(cls) -> CT_NotesMaster:
"""Return a new `p:notesMaster` element based on the built-in default template."""
return cast(CT_NotesMaster, parse_from_template("notesMaster"))
class CT_NotesSlide(_BaseSlideElement):
"""`p:notes` element, root of a notes slide part."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
@classmethod
def new(cls) -> CT_NotesSlide:
"""Return a new ``<p:notes>`` element based on the default template.
Note that the template does not include placeholders, which must be subsequently cloned
from the notes master.
"""
return cast(CT_NotesSlide, parse_from_template("notes"))
class CT_Slide(_BaseSlideElement):
"""`p:sld` element, root element of a slide part (XML document)."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:])
timing = ZeroOrOne("p:timing", successors=_tag_seq[4:])
del _tag_seq
@classmethod
def new(cls) -> CT_Slide:
"""Return new `p:sld` element configured as base slide shape."""
return cast(CT_Slide, parse_xml(cls._sld_xml()))
@property
def bg(self):
"""Return `p:bg` grandchild or None if not present."""
return self.cSld.bg
def get_or_add_childTnLst(self):
"""Return parent element for a new `p:video` child element.
The `p:video` element causes play controls to appear under a video
shape (pic shape containing video). There can be more than one video
shape on a slide, which causes the precondition to vary. It needs to
handle the case when there is no `p:sld/p:timing` element and when
that element already exists. If the case isn't simple, it just nukes
what's there and adds a fresh one. This could theoretically remove
desired existing timing information, but there isn't any evidence
available to me one way or the other, so I've taken the simple
approach.
"""
childTnLst = self._childTnLst
if childTnLst is None:
childTnLst = self._add_childTnLst()
return childTnLst
def _add_childTnLst(self):
"""Add `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
Any existing `p:timing` child element is ruthlessly removed and
replaced.
"""
self.remove(self.get_or_add_timing())
timing = parse_xml(self._childTnLst_timing_xml())
self._insert_timing(timing)
return timing.xpath("./p:tnLst/p:par/p:cTn/p:childTnLst")[0]
@property
def _childTnLst(self):
"""Return `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
Return None if that element is not present.
"""
childTnLsts = self.xpath("./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst")
if not childTnLsts:
return None
return childTnLsts[0]
@staticmethod
def _childTnLst_timing_xml():
return (
"<p:timing %s>\n"
" <p:tnLst>\n"
" <p:par>\n"
' <p:cTn id="1" dur="indefinite" restart="never" nodeType="'
'tmRoot">\n'
" <p:childTnLst/>\n"
" </p:cTn>\n"
" </p:par>\n"
" </p:tnLst>\n"
"</p:timing>" % nsdecls("p")
)
@staticmethod
def _sld_xml():
return (
"<p:sld %s>\n"
" <p:cSld>\n"
" <p:spTree>\n"
" <p:nvGrpSpPr>\n"
' <p:cNvPr id="1" name=""/>\n'
" <p:cNvGrpSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvGrpSpPr>\n"
" <p:grpSpPr/>\n"
" </p:spTree>\n"
" </p:cSld>\n"
" <p:clrMapOvr>\n"
" <a:masterClrMapping/>\n"
" </p:clrMapOvr>\n"
"</p:sld>" % nsdecls("a", "p", "r")
)
class CT_SlideLayout(_BaseSlideElement):
"""`p:sldLayout` element, root of a slide layout part."""
_tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq
class CT_SlideLayoutIdList(BaseOxmlElement):
"""`p:sldLayoutIdLst` element, child of `p:sldMaster`.
Contains references to the slide layouts that inherit from the slide master.
"""
sldLayoutId_lst: list[CT_SlideLayoutIdListEntry]
sldLayoutId = ZeroOrMore("p:sldLayoutId")
class CT_SlideLayoutIdListEntry(BaseOxmlElement):
"""`p:sldLayoutId` element, child of `p:sldLayoutIdLst`.
Contains a reference to a slide layout.
"""
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
class CT_SlideMaster(_BaseSlideElement):
"""`p:sldMaster` element, root of a slide master part."""
get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList]
_tag_seq = (
"p:cSld",
"p:clrMap",
"p:sldLayoutIdLst",
"p:transition",
"p:timing",
"p:hf",
"p:txStyles",
"p:extLst",
)
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldLayoutIdLst", successors=_tag_seq[3:]
)
del _tag_seq
class CT_SlideTiming(BaseOxmlElement):
"""`p:timing` element, specifying animations and timed behaviors."""
_tag_seq = ("p:tnLst", "p:bldLst", "p:extLst")
tnLst = ZeroOrOne("p:tnLst", successors=_tag_seq[1:])
del _tag_seq
class CT_TimeNodeList(BaseOxmlElement):
"""`p:tnLst` or `p:childTnList` element."""
def add_video(self, shape_id):
"""Add a new `p:video` child element for movie having *shape_id*."""
video_xml = (
"<p:video %s>\n"
' <p:cMediaNode vol="80000">\n'
' <p:cTn id="%d" fill="hold" display="0">\n'
" <p:stCondLst>\n"
' <p:cond delay="indefinite"/>\n'
" </p:stCondLst>\n"
" </p:cTn>\n"
" <p:tgtEl>\n"
' <p:spTgt spid="%d"/>\n'
" </p:tgtEl>\n"
" </p:cMediaNode>\n"
"</p:video>\n" % (nsdecls("p"), self._next_cTn_id, shape_id)
)
video = parse_xml(video_xml)
self.append(video)
@property
def _next_cTn_id(self):
"""Return the next available unique ID (int) for p:cTn element."""
cTn_id_strs = self.xpath("/p:sld/p:timing//p:cTn/@id")
ids = [int(id_str) for id_str in cTn_id_strs]
return max(ids) + 1
class CT_TLMediaNodeVideo(BaseOxmlElement):
"""`p:video` element, specifying video media details."""
_tag_seq = ("p:cMediaNode",)
cMediaNode = OneAndOnlyOne("p:cMediaNode")
del _tag_seq
@@ -0,0 +1,588 @@
"""Custom element classes for table-related XML elements"""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Iterator, cast
from pptx.enum.text import MSO_VERTICAL_ANCHOR
from pptx.oxml import parse_xml
from pptx.oxml.dml.fill import CT_GradientFillProperties
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import ST_Coordinate, ST_Coordinate32, XsdBoolean, XsdInt
from pptx.oxml.text import CT_TextBody
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OneAndOnlyOne,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
ZeroOrOneChoice,
)
from pptx.util import Emu, lazyproperty
if TYPE_CHECKING:
from pptx.util import Length
class CT_Table(BaseOxmlElement):
"""`a:tbl` custom element class"""
get_or_add_tblPr: Callable[[], CT_TableProperties]
tr_lst: list[CT_TableRow]
_add_tr: Callable[..., CT_TableRow]
_tag_seq = ("a:tblPr", "a:tblGrid", "a:tr")
tblPr: CT_TableProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:tblPr", successors=_tag_seq[1:]
)
tblGrid: CT_TableGrid = OneAndOnlyOne("a:tblGrid") # pyright: ignore[reportAssignmentType]
tr = ZeroOrMore("a:tr", successors=_tag_seq[3:])
del _tag_seq
def add_tr(self, height: Length) -> CT_TableRow:
"""Return a newly created `a:tr` child element having its `h` attribute set to `height`."""
return self._add_tr(h=height)
@property
def bandCol(self) -> bool:
return self._get_boolean_property("bandCol")
@bandCol.setter
def bandCol(self, value: bool):
self._set_boolean_property("bandCol", value)
@property
def bandRow(self) -> bool:
return self._get_boolean_property("bandRow")
@bandRow.setter
def bandRow(self, value: bool):
self._set_boolean_property("bandRow", value)
@property
def firstCol(self) -> bool:
return self._get_boolean_property("firstCol")
@firstCol.setter
def firstCol(self, value: bool):
self._set_boolean_property("firstCol", value)
@property
def firstRow(self) -> bool:
return self._get_boolean_property("firstRow")
@firstRow.setter
def firstRow(self, value: bool):
self._set_boolean_property("firstRow", value)
def iter_tcs(self) -> Iterator[CT_TableCell]:
"""Generate each `a:tc` element in this tbl.
`a:tc` elements are generated left-to-right, top-to-bottom.
"""
return (tc for tr in self.tr_lst for tc in tr.tc_lst)
@property
def lastCol(self) -> bool:
return self._get_boolean_property("lastCol")
@lastCol.setter
def lastCol(self, value: bool):
self._set_boolean_property("lastCol", value)
@property
def lastRow(self) -> bool:
return self._get_boolean_property("lastRow")
@lastRow.setter
def lastRow(self, value: bool):
self._set_boolean_property("lastRow", value)
@classmethod
def new_tbl(
cls, rows: int, cols: int, width: int, height: int, tableStyleId: str | None = None
) -> CT_Table:
"""Return a new `p:tbl` element tree."""
# working hypothesis is this is the default table style GUID
if tableStyleId is None:
tableStyleId = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
xml = cls._tbl_tmpl() % (tableStyleId)
tbl = cast(CT_Table, parse_xml(xml))
# add specified number of rows and columns
rowheight = height // rows
colwidth = width // cols
for col in range(cols):
# adjust width of last col to absorb any div error
if col == cols - 1:
colwidth = width - ((cols - 1) * colwidth)
tbl.tblGrid.add_gridCol(width=Emu(colwidth))
for row in range(rows):
# adjust height of last row to absorb any div error
if row == rows - 1:
rowheight = height - ((rows - 1) * rowheight)
tr = tbl.add_tr(height=Emu(rowheight))
for col in range(cols):
tr.add_tc()
return tbl
def tc(self, row_idx: int, col_idx: int) -> CT_TableCell:
"""Return `a:tc` element at `row_idx`, `col_idx`."""
return self.tr_lst[row_idx].tc_lst[col_idx]
def _get_boolean_property(self, propname: str) -> bool:
"""Generalized getter for the boolean properties on the `a:tblPr` child element.
Defaults to False if `propname` attribute is missing or `a:tblPr` element itself is not
present.
"""
tblPr = self.tblPr
if tblPr is None:
return False
propval = getattr(tblPr, propname)
return {True: True, False: False, None: False}[propval]
def _set_boolean_property(self, propname: str, value: bool) -> None:
"""Generalized setter for boolean properties on the `a:tblPr` child element.
Sets `propname` attribute appropriately based on `value`. If `value` is True, the
attribute is set to "1"; a tblPr child element is added if necessary. If `value` is False,
the `propname` attribute is removed if present, allowing its default value of False to be
its effective value.
"""
if value not in (True, False):
raise ValueError("assigned value must be either True or False, got %s" % value)
tblPr = self.get_or_add_tblPr()
setattr(tblPr, propname, value)
@classmethod
def _tbl_tmpl(cls):
return (
"<a:tbl %s>\n"
' <a:tblPr firstRow="1" bandRow="1">\n'
" <a:tableStyleId>%s</a:tableStyleId>\n"
" </a:tblPr>\n"
" <a:tblGrid/>\n"
"</a:tbl>" % (nsdecls("a"), "%s")
)
class CT_TableCell(BaseOxmlElement):
"""`a:tc` custom element class"""
get_or_add_tcPr: Callable[[], CT_TableCellProperties]
get_or_add_txBody: Callable[[], CT_TextBody]
_tag_seq = ("a:txBody", "a:tcPr", "a:extLst")
txBody: CT_TextBody | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:txBody", successors=_tag_seq[1:]
)
tcPr: CT_TableCellProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:tcPr", successors=_tag_seq[2:]
)
del _tag_seq
gridSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"gridSpan", XsdInt, default=1
)
rowSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"rowSpan", XsdInt, default=1
)
hMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"hMerge", XsdBoolean, default=False
)
vMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"vMerge", XsdBoolean, default=False
)
@property
def anchor(self) -> MSO_VERTICAL_ANCHOR | None:
"""String held in `anchor` attribute of `a:tcPr` child element of this `a:tc` element."""
if self.tcPr is None:
return None
return self.tcPr.anchor
@anchor.setter
def anchor(self, anchor_enum_idx: MSO_VERTICAL_ANCHOR | None):
"""Set value of anchor attribute on `a:tcPr` child element."""
if anchor_enum_idx is None and self.tcPr is None:
return
tcPr = self.get_or_add_tcPr()
tcPr.anchor = anchor_enum_idx
def append_ps_from(self, spanned_tc: CT_TableCell):
"""Append `a:p` elements taken from `spanned_tc`.
Any non-empty paragraph elements in `spanned_tc` are removed and appended to the
text-frame of this cell. If `spanned_tc` is left with no content after this process, a
single empty `a:p` element is added to ensure the cell is compliant with the spec.
"""
source_txBody = spanned_tc.get_or_add_txBody()
target_txBody = self.get_or_add_txBody()
# ---if source is empty, there's nothing to do---
if source_txBody.is_empty:
return
# ---a single empty paragraph in target is overwritten---
if target_txBody.is_empty:
target_txBody.clear_content()
for p in source_txBody.p_lst:
target_txBody.append(p)
# ---neither source nor target can be left without ps---
source_txBody.unclear_content()
target_txBody.unclear_content()
@property
def col_idx(self) -> int:
"""Offset of this cell's column in its table."""
# ---tc elements come before any others in `a:tr` element---
return cast(CT_TableRow, self.getparent()).index(self)
@property
def is_merge_origin(self) -> bool:
"""True if cell is top-left in merged cell range."""
if self.gridSpan > 1 and not self.vMerge:
return True
return self.rowSpan > 1 and not self.hMerge
@property
def is_spanned(self) -> bool:
"""True if cell is in merged cell range but not merge origin cell."""
return self.hMerge or self.vMerge
@property
def marT(self) -> Length:
"""Top margin for this cell.
This value is stored in the `marT` attribute of the `a:tcPr` child element of this `a:tc`.
Read/write. If the attribute is not present, the default value `45720` (0.05 inches) is
returned for top and bottom; `91440` (0.10 inches) is the default for left and right.
Assigning |None| to any `marX` property clears that attribute from the element,
effectively setting it to the default value.
"""
return self._get_marX("marT", Emu(45720))
@marT.setter
def marT(self, value: Length | None):
self._set_marX("marT", value)
@property
def marR(self) -> Length:
"""Right margin value represented in `marR` attribute."""
return self._get_marX("marR", Emu(91440))
@marR.setter
def marR(self, value: Length | None):
self._set_marX("marR", value)
@property
def marB(self) -> Length:
"""Bottom margin value represented in `marB` attribute."""
return self._get_marX("marB", Emu(45720))
@marB.setter
def marB(self, value: Length | None):
self._set_marX("marB", value)
@property
def marL(self) -> Length:
"""Left margin value represented in `marL` attribute."""
return self._get_marX("marL", Emu(91440))
@marL.setter
def marL(self, value: Length | None):
self._set_marX("marL", value)
@classmethod
def new(cls) -> CT_TableCell:
"""Return a new `a:tc` element subtree."""
return cast(
CT_TableCell,
parse_xml(
f"<a:tc {nsdecls('a')}>\n"
f" <a:txBody>\n"
f" <a:bodyPr/>\n"
f" <a:lstStyle/>\n"
f" <a:p/>\n"
f" </a:txBody>\n"
f" <a:tcPr/>\n"
f"</a:tc>"
),
)
@property
def row_idx(self) -> int:
"""Offset of this cell's row in its table."""
return cast(CT_TableRow, self.getparent()).row_idx
@property
def tbl(self) -> CT_Table:
"""Table element this cell belongs to."""
return cast(CT_Table, self.xpath("ancestor::a:tbl")[0])
@property
def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
"""str text contained in cell"""
# ---note this shadows lxml _Element.text---
txBody = self.txBody
if txBody is None:
return ""
return "\n".join([p.text for p in txBody.p_lst])
def _get_marX(self, attr_name: str, default: Length) -> Length:
"""Generalized method to get margin values."""
if self.tcPr is None:
return Emu(default)
return Emu(int(self.tcPr.get(attr_name, default)))
def _new_txBody(self) -> CT_TextBody:
return CT_TextBody.new_a_txBody()
def _set_marX(self, marX: str, value: Length | None) -> None:
"""Set value of marX attribute on `a:tcPr` child element.
If `marX` is |None|, the marX attribute is removed. `marX` is a string, one of `('marL',
'marR', 'marT', 'marB')`.
"""
if value is None and self.tcPr is None:
return
tcPr = self.get_or_add_tcPr()
setattr(tcPr, marX, value)
class CT_TableCellProperties(BaseOxmlElement):
"""`a:tcPr` custom element class"""
eg_fillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:blipFill"),
Choice("a:pattFill"),
Choice("a:grpFill"),
),
successors=("a:headers", "a:extLst"),
)
anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"anchor", MSO_VERTICAL_ANCHOR
)
marL: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"marL", ST_Coordinate32
)
marR: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"marR", ST_Coordinate32
)
marT: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"marT", ST_Coordinate32
)
marB: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"marB", ST_Coordinate32
)
def _new_gradFill(self):
return CT_GradientFillProperties.new_gradFill()
class CT_TableCol(BaseOxmlElement):
"""`a:gridCol` custom element class."""
w: Length = RequiredAttribute("w", ST_Coordinate) # pyright: ignore[reportAssignmentType]
class CT_TableGrid(BaseOxmlElement):
"""`a:tblGrid` custom element class."""
gridCol_lst: list[CT_TableCol]
_add_gridCol: Callable[..., CT_TableCol]
gridCol = ZeroOrMore("a:gridCol")
def add_gridCol(self, width: Length) -> CT_TableCol:
"""A newly appended `a:gridCol` child element having its `w` attribute set to `width`."""
return self._add_gridCol(w=width)
class CT_TableProperties(BaseOxmlElement):
"""`a:tblPr` custom element class."""
bandRow = OptionalAttribute("bandRow", XsdBoolean, default=False)
bandCol = OptionalAttribute("bandCol", XsdBoolean, default=False)
firstRow = OptionalAttribute("firstRow", XsdBoolean, default=False)
firstCol = OptionalAttribute("firstCol", XsdBoolean, default=False)
lastRow = OptionalAttribute("lastRow", XsdBoolean, default=False)
lastCol = OptionalAttribute("lastCol", XsdBoolean, default=False)
class CT_TableRow(BaseOxmlElement):
"""`a:tr` custom element class."""
tc_lst: list[CT_TableCell]
_add_tc: Callable[[], CT_TableCell]
tc = ZeroOrMore("a:tc", successors=("a:extLst",))
h: Length = RequiredAttribute("h", ST_Coordinate) # pyright: ignore[reportAssignmentType]
def add_tc(self) -> CT_TableCell:
"""A newly added minimal valid `a:tc` child element."""
return self._add_tc()
@property
def row_idx(self) -> int:
"""Offset of this row in its table."""
return cast(CT_Table, self.getparent()).tr_lst.index(self)
def _new_tc(self):
return CT_TableCell.new()
class TcRange(object):
"""A 2D block of `a:tc` cell elements in a table.
This object assumes the structure of the underlying table does not change during its lifetime.
Structural changes in this context would be insertion or removal of rows or columns.
The client is expected to create, use, and then abandon an instance in the context of a single
user operation that is known to have no structural side-effects of this type.
"""
def __init__(self, tc: CT_TableCell, other_tc: CT_TableCell):
self._tc = tc
self._other_tc = other_tc
@classmethod
def from_merge_origin(cls, tc: CT_TableCell):
"""Return instance created from merge-origin tc element."""
other_tc = tc.tbl.tc(
tc.row_idx + tc.rowSpan - 1, # ---other_row_idx
tc.col_idx + tc.gridSpan - 1, # ---other_col_idx
)
return cls(tc, other_tc)
@lazyproperty
def contains_merged_cell(self) -> bool:
"""True if one or more cells in range are part of a merged cell."""
for tc in self.iter_tcs():
if tc.gridSpan > 1:
return True
if tc.rowSpan > 1:
return True
if tc.hMerge:
return True
if tc.vMerge:
return True
return False
@lazyproperty
def dimensions(self) -> tuple[int, int]:
"""(row_count, col_count) pair describing size of range."""
_, _, width, height = self._extents
return height, width
@lazyproperty
def in_same_table(self):
"""True if both cells provided to constructor are in same table."""
if self._tc.tbl is self._other_tc.tbl:
return True
return False
def iter_except_left_col_tcs(self):
"""Generate each `a:tc` element not in leftmost column of range."""
for tr in self._tbl.tr_lst[self._top : self._bottom]:
for tc in tr.tc_lst[self._left + 1 : self._right]:
yield tc
def iter_except_top_row_tcs(self):
"""Generate each `a:tc` element in non-first rows of range."""
for tr in self._tbl.tr_lst[self._top + 1 : self._bottom]:
for tc in tr.tc_lst[self._left : self._right]:
yield tc
def iter_left_col_tcs(self):
"""Generate each `a:tc` element in leftmost column of range."""
col_idx = self._left
for tr in self._tbl.tr_lst[self._top : self._bottom]:
yield tr.tc_lst[col_idx]
def iter_tcs(self):
"""Generate each `a:tc` element in this range.
Cell elements are generated left-to-right, top-to-bottom.
"""
return (
tc
for tr in self._tbl.tr_lst[self._top : self._bottom]
for tc in tr.tc_lst[self._left : self._right]
)
def iter_top_row_tcs(self):
"""Generate each `a:tc` element in topmost row of range."""
tr = self._tbl.tr_lst[self._top]
for tc in tr.tc_lst[self._left : self._right]:
yield tc
def move_content_to_origin(self):
"""Move all paragraphs in range to origin cell."""
tcs = list(self.iter_tcs())
origin_tc = tcs[0]
for spanned_tc in tcs[1:]:
origin_tc.append_ps_from(spanned_tc)
@lazyproperty
def _bottom(self):
"""Index of row following last row of range"""
_, top, _, height = self._extents
return top + height
@lazyproperty
def _extents(self) -> tuple[int, int, int, int]:
"""A (left, top, width, height) tuple describing range extents.
Note this is normalized to accommodate the various orderings of the corner cells provided
on construction, which may be in any of four configurations such as (top-left,
bottom-right), (bottom-left, top-right), etc.
"""
def start_and_size(idx: int, other_idx: int) -> tuple[int, int]:
"""Return beginning and length of range based on two indexes."""
return min(idx, other_idx), abs(idx - other_idx) + 1
tc, other_tc = self._tc, self._other_tc
left, width = start_and_size(tc.col_idx, other_tc.col_idx)
top, height = start_and_size(tc.row_idx, other_tc.row_idx)
return left, top, width, height
@lazyproperty
def _left(self):
"""Index of leftmost column in range."""
left, _, _, _ = self._extents
return left
@lazyproperty
def _right(self):
"""Index of column following the last column in range."""
left, _, width, _ = self._extents
return left + width
@lazyproperty
def _tbl(self):
"""`a:tbl` element containing this cell range."""
return self._tc.tbl
@lazyproperty
def _top(self):
"""Index of topmost row in range."""
_, top, _, _ = self._extents
return top
@@ -0,0 +1,618 @@
"""Custom element classes for text-related XML elements"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Callable, cast
from pptx.enum.lang import MSO_LANGUAGE_ID
from pptx.enum.text import (
MSO_AUTO_SIZE,
MSO_TEXT_UNDERLINE_TYPE,
MSO_VERTICAL_ANCHOR,
PP_PARAGRAPH_ALIGNMENT,
)
from pptx.exc import InvalidXmlError
from pptx.oxml import parse_xml
from pptx.oxml.dml.fill import CT_GradientFillProperties
from pptx.oxml.ns import nsdecls
from pptx.oxml.simpletypes import (
ST_Coordinate32,
ST_TextFontScalePercentOrPercentString,
ST_TextFontSize,
ST_TextIndentLevelType,
ST_TextSpacingPercentOrPercentString,
ST_TextSpacingPoint,
ST_TextTypeface,
ST_TextWrappingType,
XsdBoolean,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Choice,
OneAndOnlyOne,
OneOrMore,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
ZeroOrOneChoice,
)
from pptx.util import Emu, Length
if TYPE_CHECKING:
from pptx.oxml.action import CT_Hyperlink
class CT_RegularTextRun(BaseOxmlElement):
"""`a:r` custom element class"""
get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:rPr", successors=("a:t",)
)
t: BaseOxmlElement = OneAndOnlyOne("a:t") # pyright: ignore[reportAssignmentType]
@property
def text(self) -> str:
"""All text of (required) `a:t` child."""
text = self.t.text
# -- t.text is None when t element is empty, e.g. '<a:t/>' --
return text or ""
@text.setter
def text(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride]
self.t.text = self._escape_ctrl_chars(value)
@staticmethod
def _escape_ctrl_chars(s: str) -> str:
"""Return str after replacing each control character with a plain-text escape.
For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab
(x09) and line-feed (x0A) are not escaped. All other characters in the range
x00-x1F are escaped.
"""
return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s)
class CT_TextBody(BaseOxmlElement):
"""`p:txBody` custom element class.
Also used for `c:txPr` in charts and perhaps other elements.
"""
add_p: Callable[[], CT_TextParagraph]
p_lst: list[CT_TextParagraph]
bodyPr: CT_TextBodyProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
"a:bodyPr"
)
p: CT_TextParagraph = OneOrMore("a:p") # pyright: ignore[reportAssignmentType]
def clear_content(self):
"""Remove all `a:p` children, but leave any others.
cf. lxml `_Element.clear()` method which removes all children.
"""
for p in self.p_lst:
self.remove(p)
@property
def defRPr(self) -> CT_TextCharacterProperties:
"""`a:defRPr` element of required first `p` child, added with its ancestors if not present.
Used when element is a ``c:txPr`` in a chart and the `p` element is used only to specify
formatting, not content.
"""
p = self.p_lst[0]
pPr = p.get_or_add_pPr()
defRPr = pPr.get_or_add_defRPr()
return defRPr
@property
def is_empty(self) -> bool:
"""True if only a single empty `a:p` element is present."""
ps = self.p_lst
if len(ps) > 1:
return False
if not ps:
raise InvalidXmlError("p:txBody must have at least one a:p")
if ps[0].text != "":
return False
return True
@classmethod
def new(cls):
"""Return a new `p:txBody` element tree."""
xml = cls._txBody_tmpl()
txBody = parse_xml(xml)
return txBody
@classmethod
def new_a_txBody(cls) -> CT_TextBody:
"""Return a new `a:txBody` element tree.
Suitable for use in a table cell and possibly other situations.
"""
xml = cls._a_txBody_tmpl()
txBody = cast(CT_TextBody, parse_xml(xml))
return txBody
@classmethod
def new_p_txBody(cls):
"""Return a new `p:txBody` element tree, suitable for use in an `p:sp` element."""
xml = cls._p_txBody_tmpl()
return parse_xml(xml)
@classmethod
def new_txPr(cls):
"""Return a `c:txPr` element tree.
Suitable for use in a chart object like data labels or tick labels.
"""
xml = (
"<c:txPr %s>\n"
" <a:bodyPr/>\n"
" <a:lstStyle/>\n"
" <a:p>\n"
" <a:pPr>\n"
" <a:defRPr/>\n"
" </a:pPr>\n"
" </a:p>\n"
"</c:txPr>\n"
) % nsdecls("c", "a")
txPr = parse_xml(xml)
return txPr
def unclear_content(self):
"""Ensure p:txBody has at least one a:p child.
Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec
(single empty paragraph).
"""
if len(self.p_lst) > 0:
return
self.add_p()
@classmethod
def _a_txBody_tmpl(cls):
return "<a:txBody %s>\n" " <a:bodyPr/>\n" " <a:p/>\n" "</a:txBody>\n" % (nsdecls("a"))
@classmethod
def _p_txBody_tmpl(cls):
return (
"<p:txBody %s>\n" " <a:bodyPr/>\n" " <a:p/>\n" "</p:txBody>\n" % (nsdecls("p", "a"))
)
@classmethod
def _txBody_tmpl(cls):
return (
"<p:txBody %s>\n"
" <a:bodyPr/>\n"
" <a:lstStyle/>\n"
" <a:p/>\n"
"</p:txBody>\n" % (nsdecls("a", "p"))
)
class CT_TextBodyProperties(BaseOxmlElement):
"""`a:bodyPr` custom element class."""
_add_noAutofit: Callable[[], BaseOxmlElement]
_add_normAutofit: Callable[[], CT_TextNormalAutofit]
_add_spAutoFit: Callable[[], BaseOxmlElement]
_remove_eg_textAutoFit: Callable[[], None]
noAutofit: BaseOxmlElement | None
normAutofit: CT_TextNormalAutofit | None
spAutoFit: BaseOxmlElement | None
eg_textAutoFit = ZeroOrOneChoice(
(Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")),
successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"),
)
lIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"lIns", ST_Coordinate32, default=Emu(91440)
)
tIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"tIns", ST_Coordinate32, default=Emu(45720)
)
rIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"rIns", ST_Coordinate32, default=Emu(91440)
)
bIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"bIns", ST_Coordinate32, default=Emu(45720)
)
anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"anchor", MSO_VERTICAL_ANCHOR
)
wrap: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"wrap", ST_TextWrappingType
)
@property
def autofit(self):
"""The autofit setting for the text frame, a member of the `MSO_AUTO_SIZE` enumeration."""
if self.noAutofit is not None:
return MSO_AUTO_SIZE.NONE
if self.normAutofit is not None:
return MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
if self.spAutoFit is not None:
return MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
return None
@autofit.setter
def autofit(self, value: MSO_AUTO_SIZE | None):
if value is not None and value not in MSO_AUTO_SIZE:
raise ValueError(
f"only None or a member of the MSO_AUTO_SIZE enumeration can be assigned to"
f" CT_TextBodyProperties.autofit, got {value}"
)
self._remove_eg_textAutoFit()
if value == MSO_AUTO_SIZE.NONE:
self._add_noAutofit()
elif value == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE:
self._add_normAutofit()
elif value == MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT:
self._add_spAutoFit()
class CT_TextCharacterProperties(BaseOxmlElement):
"""Custom element class for `a:rPr`, `a:defRPr`, and `a:endParaRPr`.
'rPr' is short for 'run properties', and it corresponds to the |Font| proxy class.
"""
get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
get_or_add_latin: Callable[[], CT_TextFont]
_remove_latin: Callable[[], None]
_remove_hlinkClick: Callable[[], None]
eg_fillProperties = ZeroOrOneChoice(
(
Choice("a:noFill"),
Choice("a:solidFill"),
Choice("a:gradFill"),
Choice("a:blipFill"),
Choice("a:pattFill"),
Choice("a:grpFill"),
),
successors=(
"a:effectLst",
"a:effectDag",
"a:highlight",
"a:uLnTx",
"a:uLn",
"a:uFillTx",
"a:uFill",
"a:latin",
"a:ea",
"a:cs",
"a:sym",
"a:hlinkClick",
"a:hlinkMouseOver",
"a:rtl",
"a:extLst",
),
)
latin: CT_TextFont | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:latin",
successors=(
"a:ea",
"a:cs",
"a:sym",
"a:hlinkClick",
"a:hlinkMouseOver",
"a:rtl",
"a:extLst",
),
)
hlinkClick: CT_Hyperlink | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")
)
lang: MSO_LANGUAGE_ID | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"lang", MSO_LANGUAGE_ID
)
sz: int | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"sz", ST_TextFontSize
)
b: bool | None = OptionalAttribute("b", XsdBoolean) # pyright: ignore[reportAssignmentType]
i: bool | None = OptionalAttribute("i", XsdBoolean) # pyright: ignore[reportAssignmentType]
u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"u", MSO_TEXT_UNDERLINE_TYPE
)
def _new_gradFill(self):
return CT_GradientFillProperties.new_gradFill()
def add_hlinkClick(self, rId: str) -> CT_Hyperlink:
"""Add an `a:hlinkClick` child element with r:id attribute set to `rId`."""
hlinkClick = self.get_or_add_hlinkClick()
hlinkClick.rId = rId
return hlinkClick
class CT_TextField(BaseOxmlElement):
"""`a:fld` field element, for either a slide number or date field."""
get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:rPr", successors=("a:pPr", "a:t")
)
t: BaseOxmlElement | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:t", successors=()
)
@property
def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
"""The text of the `a:t` child element."""
t = self.t
if t is None:
return ""
return t.text or ""
class CT_TextFont(BaseOxmlElement):
"""Custom element class for `a:latin`, `a:ea`, `a:cs`, and `a:sym`.
These occur as child elements of CT_TextCharacterProperties, e.g. `a:rPr`.
"""
typeface: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"typeface", ST_TextTypeface
)
class CT_TextLineBreak(BaseOxmlElement):
"""`a:br` line break element"""
get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
rPr = ZeroOrOne("a:rPr", successors=())
@property
def text(self): # pyright: ignore[reportIncompatibleMethodOverride]
"""Unconditionally a single vertical-tab character.
A line break element can contain no text other than the implicit line feed it
represents.
"""
return "\v"
class CT_TextNormalAutofit(BaseOxmlElement):
"""`a:normAutofit` element specifying fit text to shape font reduction, etc."""
fontScale = OptionalAttribute(
"fontScale", ST_TextFontScalePercentOrPercentString, default=100.0
)
class CT_TextParagraph(BaseOxmlElement):
"""`a:p` custom element class"""
get_or_add_endParaRPr: Callable[[], CT_TextCharacterProperties]
get_or_add_pPr: Callable[[], CT_TextParagraphProperties]
r_lst: list[CT_RegularTextRun]
_add_br: Callable[[], CT_TextLineBreak]
_add_r: Callable[[], CT_RegularTextRun]
pPr: CT_TextParagraphProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")
)
r = ZeroOrMore("a:r", successors=("a:endParaRPr",))
br = ZeroOrMore("a:br", successors=("a:endParaRPr",))
endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne(
"a:endParaRPr", successors=()
) # pyright: ignore[reportAssignmentType]
def add_br(self) -> CT_TextLineBreak:
"""Return a newly appended `a:br` element."""
return self._add_br()
def add_r(self, text: str | None = None) -> CT_RegularTextRun:
"""Return a newly appended `a:r` element."""
r = self._add_r()
if text:
r.text = text
return r
def append_text(self, text: str):
"""Append `a:r` and `a:br` elements to `p` based on `text`.
Any `\n` or `\v` (vertical-tab) characters in `text` delimit `a:r` (run) elements and
themselves are translated to `a:br` (line-break) elements. The vertical-tab character
appears in clipboard text from PowerPoint at "soft" line-breaks (new-line, but not new
paragraph).
"""
for idx, r_str in enumerate(re.split("\n|\v", text)):
# ---breaks are only added _between_ items, not at start---
if idx > 0:
self.add_br()
# ---runs that would be empty are not added---
if r_str:
self.add_r(r_str)
@property
def content_children(self) -> tuple[CT_RegularTextRun | CT_TextLineBreak | CT_TextField, ...]:
"""Sequence containing text-container child elements of this `a:p` element.
These include `a:r`, `a:br`, and `a:fld`.
"""
return tuple(
e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField))
)
@property
def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
"""str text contained in this paragraph."""
# ---note this shadows the lxml _Element.text---
return "".join([child.text for child in self.content_children])
def _new_r(self):
r_xml = "<a:r %s><a:t/></a:r>" % nsdecls("a")
return parse_xml(r_xml)
class CT_TextParagraphProperties(BaseOxmlElement):
"""`a:pPr` custom element class."""
get_or_add_defRPr: Callable[[], CT_TextCharacterProperties]
_add_lnSpc: Callable[[], CT_TextSpacing]
_add_spcAft: Callable[[], CT_TextSpacing]
_add_spcBef: Callable[[], CT_TextSpacing]
_remove_lnSpc: Callable[[], None]
_remove_spcAft: Callable[[], None]
_remove_spcBef: Callable[[], None]
_tag_seq = (
"a:lnSpc",
"a:spcBef",
"a:spcAft",
"a:buClrTx",
"a:buClr",
"a:buSzTx",
"a:buSzPct",
"a:buSzPts",
"a:buFontTx",
"a:buFont",
"a:buNone",
"a:buAutoNum",
"a:buChar",
"a:buBlip",
"a:tabLst",
"a:defRPr",
"a:extLst",
)
lnSpc: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:lnSpc", successors=_tag_seq[1:]
)
spcBef: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:spcBef", successors=_tag_seq[2:]
)
spcAft: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:spcAft", successors=_tag_seq[3:]
)
defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:defRPr", successors=_tag_seq[16:]
)
lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"lvl", ST_TextIndentLevelType, default=0
)
algn: PP_PARAGRAPH_ALIGNMENT | None = OptionalAttribute(
"algn", PP_PARAGRAPH_ALIGNMENT
) # pyright: ignore[reportAssignmentType]
del _tag_seq
@property
def line_spacing(self) -> float | Length | None:
"""The spacing between baselines of successive lines in this paragraph.
A float value indicates a number of lines. A |Length| value indicates a fixed spacing.
Value is contained in `./a:lnSpc/a:spcPts/@val` or `./a:lnSpc/a:spcPct/@val`. Value is
|None| if no element is present.
"""
lnSpc = self.lnSpc
if lnSpc is None:
return None
if lnSpc.spcPts is not None:
return lnSpc.spcPts.val
return cast(CT_TextSpacingPercent, lnSpc.spcPct).val
@line_spacing.setter
def line_spacing(self, value: float | Length | None):
self._remove_lnSpc()
if value is None:
return
if isinstance(value, Length):
self._add_lnSpc().set_spcPts(value)
else:
self._add_lnSpc().set_spcPct(value)
@property
def space_after(self) -> Length | None:
"""The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`."""
spcAft = self.spcAft
if spcAft is None:
return None
spcPts = spcAft.spcPts
if spcPts is None:
return None
return spcPts.val
@space_after.setter
def space_after(self, value: Length | None):
self._remove_spcAft()
if value is not None:
self._add_spcAft().set_spcPts(value)
@property
def space_before(self):
"""The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`."""
spcBef = self.spcBef
if spcBef is None:
return None
spcPts = spcBef.spcPts
if spcPts is None:
return None
return spcPts.val
@space_before.setter
def space_before(self, value: Length | None):
self._remove_spcBef()
if value is not None:
self._add_spcBef().set_spcPts(value)
class CT_TextSpacing(BaseOxmlElement):
"""Used for `a:lnSpc`, `a:spcBef`, and `a:spcAft` elements."""
get_or_add_spcPct: Callable[[], CT_TextSpacingPercent]
get_or_add_spcPts: Callable[[], CT_TextSpacingPoint]
_remove_spcPct: Callable[[], None]
_remove_spcPts: Callable[[], None]
# this should actually be a OneAndOnlyOneChoice, but that's not
# implemented yet.
spcPct: CT_TextSpacingPercent | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:spcPct"
)
spcPts: CT_TextSpacingPoint | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:spcPts"
)
def set_spcPct(self, value: float):
"""Set spacing to `value` lines, e.g. 1.75 lines.
A ./a:spcPts child is removed if present.
"""
self._remove_spcPts()
spcPct = self.get_or_add_spcPct()
spcPct.val = value
def set_spcPts(self, value: Length):
"""Set spacing to `value` points. A ./a:spcPct child is removed if present."""
self._remove_spcPct()
spcPts = self.get_or_add_spcPts()
spcPts.val = value
class CT_TextSpacingPercent(BaseOxmlElement):
"""`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute."""
val: float = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"val", ST_TextSpacingPercentOrPercentString
)
class CT_TextSpacingPoint(BaseOxmlElement):
"""`a:spcPts` element, specifying spacing in centipoints in its `val` attribute."""
val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"val", ST_TextSpacingPoint
)
@@ -0,0 +1,29 @@
"""lxml custom element classes for theme-related XML elements."""
from __future__ import annotations
from . import parse_from_template
from .xmlchemy import BaseOxmlElement
class CT_OfficeStyleSheet(BaseOxmlElement):
"""
``<a:theme>`` element, root of a theme part
"""
_tag_seq = (
"a:themeElements",
"a:objectDefaults",
"a:extraClrSchemeLst",
"a:custClrLst",
"a:extLst",
)
del _tag_seq
@classmethod
def new_default(cls):
"""
Return a new ``<a:theme>`` element containing default settings
suitable for use with a notes master.
"""
return parse_from_template("theme")
@@ -0,0 +1,717 @@
"""Base and meta classes enabling declarative definition of custom element classes."""
from __future__ import annotations
import re
from typing import Any, Callable, Iterable, Protocol, Sequence, Type, cast
from lxml import etree
from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage]
from pptx.exc import InvalidXmlError
from pptx.oxml import oxml_parser
from pptx.oxml.ns import NamespacePrefixedTag, _nsmap, qn # pyright: ignore[reportPrivateUsage]
from pptx.util import lazyproperty
class AttributeType(Protocol):
"""Interface for an object that can act as an attribute type.
An attribute-type specifies how values are transformed to and from the XML "string" value of the
attribute.
"""
@classmethod
def from_xml(cls, xml_value: str) -> Any:
"""Transform an attribute value to a Python value."""
...
@classmethod
def to_xml(cls, value: Any) -> str:
"""Transform a Python value to a str value suitable to this XML attribute."""
...
def OxmlElement(nsptag_str: str, nsmap: dict[str, str] | None = None) -> BaseOxmlElement:
"""Return a "loose" lxml element having the tag specified by `nsptag_str`.
`nsptag_str` must contain the standard namespace prefix, e.g. 'a:tbl'. The resulting element is
an instance of the custom element class for this tag name if one is defined.
"""
nsptag = NamespacePrefixedTag(nsptag_str)
nsmap = nsmap if nsmap is not None else nsptag.nsmap
return oxml_parser.makeelement(nsptag.clark_name, nsmap=nsmap)
def serialize_for_reading(element: ElementBase):
"""
Serialize *element* to human-readable XML suitable for tests. No XML
declaration.
"""
xml = etree.tostring(element, encoding="unicode", pretty_print=True)
return XmlString(xml)
class XmlString(str):
"""Provides string comparison override suitable for serialized XML; useful for tests."""
# ' <w:xyz xmlns:a="http://ns/decl/a" attr_name="val">text</w:xyz>'
# | | || |
# +----------+------------------------------------------++-----------+
# front attrs | text
# close
_xml_elm_line_patt = re.compile(r"( *</?[\w:]+)(.*?)(/?>)([^<]*</[\w:]+>)?")
def __eq__(self, other: object) -> bool:
if not isinstance(other, str):
return False
lines = self.splitlines()
lines_other = other.splitlines()
if len(lines) != len(lines_other):
return False
for line, line_other in zip(lines, lines_other):
if not self._eq_elm_strs(line, line_other):
return False
return True
def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
def _attr_seq(self, attrs: str) -> list[str]:
"""Return a sequence of attribute strings parsed from *attrs*.
Each attribute string is stripped of whitespace on both ends.
"""
attrs = attrs.strip()
attr_lst = attrs.split()
return sorted(attr_lst)
def _eq_elm_strs(self, line: str, line_2: str) -> bool:
"""True if the element in `line_2` is XML-equivalent to the element in `line`.
In particular, the order of attributes in XML is not significant.
"""
front, attrs, close, text = self._parse_line(line)
front_2, attrs_2, close_2, text_2 = self._parse_line(line_2)
if front != front_2:
return False
if self._attr_seq(attrs) != self._attr_seq(attrs_2):
return False
if close != close_2:
return False
if text != text_2:
return False
return True
def _parse_line(self, line: str):
"""Return front, attrs, close, text 4-tuple result of parsing XML element string `line`."""
match = self._xml_elm_line_patt.match(line)
if match is None:
raise ValueError("`line` does not match pattern for an XML element")
front, attrs, close, text = [match.group(n) for n in range(1, 5)]
return front, attrs, close, text
class MetaOxmlElement(type):
"""Metaclass for BaseOxmlElement."""
def __init__(cls, clsname: str, bases: tuple[type, ...], clsdict: dict[str, Any]):
dispatchable = (
OneAndOnlyOne,
OneOrMore,
OptionalAttribute,
RequiredAttribute,
ZeroOrMore,
ZeroOrOne,
ZeroOrOneChoice,
)
for key, value in clsdict.items():
if isinstance(value, dispatchable):
value.populate_class_members(cls, key)
class BaseAttribute:
"""Base class for OptionalAttribute and RequiredAttribute, providing common methods."""
def __init__(self, attr_name: str, simple_type: type[AttributeType]):
self._attr_name = attr_name
self._simple_type = simple_type
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""
Add the appropriate methods to *element_cls*.
"""
self._element_cls = element_cls
self._prop_name = prop_name
self._add_attr_property()
def _add_attr_property(self):
"""Add a read/write `{prop_name}` property to the element class.
The property returns the interpreted value of this attribute on access and changes the
attribute value to its ST_* counterpart on assignment.
"""
property_ = property(self._getter, self._setter, None)
# assign unconditionally to overwrite element name definition
setattr(self._element_cls, self._prop_name, property_)
@property
def _clark_name(self):
if ":" in self._attr_name:
return qn(self._attr_name)
return self._attr_name
@property
def _getter(self) -> Callable[[BaseOxmlElement], Any]:
"""Callable suitable for the "get" side of the attribute property descriptor."""
raise NotImplementedError("must be implemented by each subclass")
@property
def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
"""Callable suitable for the "set" side of the attribute property descriptor."""
raise NotImplementedError("must be implemented by each subclass")
class OptionalAttribute(BaseAttribute):
"""Defines an optional attribute on a custom element class.
An optional attribute returns a default value when not present for reading. When assigned
|None|, the attribute is removed.
"""
def __init__(self, attr_name: str, simple_type: type[AttributeType], default: Any = None):
super(OptionalAttribute, self).__init__(attr_name, simple_type)
self._default = default
@property
def _docstring(self):
"""
Return the string to use as the ``__doc__`` attribute of the property
for this attribute.
"""
return (
"%s type-converted value of ``%s`` attribute, or |None| (or spec"
"ified default value) if not present. Assigning the default valu"
"e causes the attribute to be removed from the element."
% (self._simple_type.__name__, self._attr_name)
)
@property
def _getter(self) -> Callable[[BaseOxmlElement], Any]:
"""Callable suitable for the "get" side of the attribute property descriptor."""
def get_attr_value(obj: BaseOxmlElement) -> Any:
attr_str_value = obj.get(self._clark_name)
if attr_str_value is None:
return self._default
return self._simple_type.from_xml(attr_str_value)
get_attr_value.__doc__ = self._docstring
return get_attr_value
@property
def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
"""Callable suitable for the "set" side of the attribute property descriptor."""
def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
# -- when an XML attribute has a default value, setting it to that default removes the
# -- attribute from the element (when it is present)
if value == self._default:
if self._clark_name in obj.attrib:
del obj.attrib[self._clark_name]
return
str_value = self._simple_type.to_xml(value)
obj.set(self._clark_name, str_value)
return set_attr_value
class RequiredAttribute(BaseAttribute):
"""Defines a required attribute on a custom element class.
A required attribute is assumed to be present for reading, so does not have a default value;
its actual value is always used. If missing on read, an |InvalidXmlError| is raised. It also
does not remove the attribute if |None| is assigned. Assigning |None| raises |TypeError| or
|ValueError|, depending on the simple type of the attribute.
"""
@property
def _getter(self) -> Callable[[BaseOxmlElement], Any]:
"""Callable suitable for the "get" side of the attribute property descriptor."""
def get_attr_value(obj: BaseOxmlElement) -> Any:
attr_str_value = obj.get(self._clark_name)
if attr_str_value is None:
raise InvalidXmlError(
"required '%s' attribute not present on element %s" % (self._attr_name, obj.tag)
)
return self._simple_type.from_xml(attr_str_value)
get_attr_value.__doc__ = self._docstring
return get_attr_value
@property
def _docstring(self):
"""
Return the string to use as the ``__doc__`` attribute of the property
for this attribute.
"""
return "%s type-converted value of ``%s`` attribute." % (
self._simple_type.__name__,
self._attr_name,
)
@property
def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
"""Callable suitable for the "set" side of the attribute property descriptor."""
def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
str_value = self._simple_type.to_xml(value)
obj.set(self._clark_name, str_value)
return set_attr_value
class _BaseChildElement:
"""Base class for the child element classes corresponding to varying cardinalities.
Subclasses include ZeroOrOne and ZeroOrMore.
"""
def __init__(self, nsptagname: str, successors: Sequence[str] = ()):
super(_BaseChildElement, self).__init__()
self._nsptagname = nsptagname
self._successors = successors
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""Baseline behavior for adding the appropriate methods to `element_cls`."""
self._element_cls = element_cls
self._prop_name = prop_name
def _add_adder(self):
"""Add an ``_add_x()`` method to the element class for this child element."""
def _add_child(obj: BaseOxmlElement, **attrs: Any):
new_method = getattr(obj, self._new_method_name)
child = new_method()
for key, value in attrs.items():
setattr(child, key, value)
insert_method = getattr(obj, self._insert_method_name)
insert_method(child)
return child
_add_child.__doc__ = (
"Add a new ``<%s>`` child element unconditionally, inserted in t"
"he correct sequence." % self._nsptagname
)
self._add_to_class(self._add_method_name, _add_child)
def _add_creator(self):
"""Add a `_new_{prop_name}()` method to the element class.
This method creates a new, empty element of the correct type, having no attributes.
"""
creator = self._creator
creator.__doc__ = (
'Return a "loose", newly created ``<%s>`` element having no attri'
"butes, text, or children." % self._nsptagname
)
self._add_to_class(self._new_method_name, creator)
def _add_getter(self):
"""Add a read-only `{prop_name}` property to the parent element class.
The property locates and returns this child element or `None` if not present.
"""
property_ = property(self._getter, None, None)
# assign unconditionally to overwrite element name definition
setattr(self._element_cls, self._prop_name, property_)
def _add_inserter(self):
"""Add an ``_insert_x()`` method to the element class for this child element."""
def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement):
obj.insert_element_before(child, *self._successors)
return child
_insert_child.__doc__ = (
"Return the passed ``<%s>`` element after inserting it as a chil"
"d in the correct sequence." % self._nsptagname
)
self._add_to_class(self._insert_method_name, _insert_child)
def _add_list_getter(self):
"""
Add a read-only ``{prop_name}_lst`` property to the element class to
retrieve a list of child elements matching this type.
"""
prop_name = f"{self._prop_name}_lst"
property_ = property(self._list_getter, None, None)
setattr(self._element_cls, prop_name, property_)
@lazyproperty
def _add_method_name(self):
return "_add_%s" % self._prop_name
def _add_to_class(self, name: str, method: Callable[..., Any]):
"""Add `method` to the target class as `name`, unless `name` is already defined there."""
if hasattr(self._element_cls, name):
return
setattr(self._element_cls, name, method)
@property
def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
"""Callable that creates a new, empty element of the child type, having no attributes."""
def new_child_element(obj: BaseOxmlElement):
return OxmlElement(self._nsptagname)
return new_child_element
@property
def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement | None]:
"""Callable suitable for the "get" side of the property descriptor.
This default getter returns the child element with matching tag name or |None| if not
present.
"""
def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement | None:
return obj.find(qn(self._nsptagname))
get_child_element.__doc__ = (
"``<%s>`` child element or |None| if not present." % self._nsptagname
)
return get_child_element
@lazyproperty
def _insert_method_name(self):
return "_insert_%s" % self._prop_name
@property
def _list_getter(self) -> Callable[[BaseOxmlElement], list[BaseOxmlElement]]:
"""Callable suitable for the "get" side of a list property descriptor."""
def get_child_element_list(obj: BaseOxmlElement) -> list[BaseOxmlElement]:
return cast("list[BaseOxmlElement]", obj.findall(qn(self._nsptagname)))
get_child_element_list.__doc__ = (
"A list containing each of the ``<%s>`` child elements, in the o"
"rder they appear." % self._nsptagname
)
return get_child_element_list
@lazyproperty
def _remove_method_name(self):
return "_remove_%s" % self._prop_name
@lazyproperty
def _new_method_name(self):
return "_new_%s" % self._prop_name
class Choice(_BaseChildElement):
"""Defines a child element belonging to a group, only one of which may appear as a child."""
@property
def nsptagname(self):
return self._nsptagname
def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride]
self, element_cls: Type[BaseOxmlElement], group_prop_name: str, successors: Sequence[str]
):
"""Add the appropriate methods to `element_cls`."""
self._element_cls = element_cls
self._group_prop_name = group_prop_name
self._successors = successors
self._add_getter()
self._add_creator()
self._add_inserter()
self._add_adder()
self._add_get_or_change_to_method()
def _add_get_or_change_to_method(self) -> None:
"""Add a `get_or_change_to_x()` method to the element class for this child element."""
def get_or_change_to_child(obj: BaseOxmlElement):
child = getattr(obj, self._prop_name)
if child is not None:
return child
remove_group_method = getattr(obj, self._remove_group_method_name)
remove_group_method()
add_method = getattr(obj, self._add_method_name)
child = add_method()
return child
get_or_change_to_child.__doc__ = (
"Return the ``<%s>`` child, replacing any other group element if" " found."
) % self._nsptagname
self._add_to_class(self._get_or_change_to_method_name, get_or_change_to_child)
@property
def _prop_name(self):
"""
Calculate property name from tag name, e.g. a:schemeClr -> schemeClr.
"""
if ":" in self._nsptagname:
start = self._nsptagname.index(":") + 1
else:
start = 0
return self._nsptagname[start:]
@lazyproperty
def _get_or_change_to_method_name(self):
return "get_or_change_to_%s" % self._prop_name
@lazyproperty
def _remove_group_method_name(self):
return "_remove_%s" % self._group_prop_name
class OneAndOnlyOne(_BaseChildElement):
"""Defines a required child element for MetaOxmlElement."""
def __init__(self, nsptagname: str):
super(OneAndOnlyOne, self).__init__(nsptagname, ())
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""
Add the appropriate methods to *element_cls*.
"""
super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name)
self._add_getter()
@property
def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
"""Callable suitable for the "get" side of the property descriptor."""
def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement:
child = obj.find(qn(self._nsptagname))
if child is None:
raise InvalidXmlError(
"required ``<%s>`` child element not present" % self._nsptagname
)
return child
get_child_element.__doc__ = "Required ``<%s>`` child element." % self._nsptagname
return get_child_element
class OneOrMore(_BaseChildElement):
"""Defines a repeating child element for MetaOxmlElement that must appear at least once."""
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""Add the appropriate methods to *element_cls*."""
super(OneOrMore, self).populate_class_members(element_cls, prop_name)
self._add_list_getter()
self._add_creator()
self._add_inserter()
self._add_adder()
self._add_public_adder()
delattr(element_cls, prop_name)
def _add_public_adder(self) -> None:
"""Add a public `.add_x()` method to the parent element class."""
def add_child(obj: BaseOxmlElement) -> BaseOxmlElement:
private_add_method = getattr(obj, self._add_method_name)
child = private_add_method()
return child
add_child.__doc__ = (
"Add a new ``<%s>`` child element unconditionally, inserted in t"
"he correct sequence." % self._nsptagname
)
self._add_to_class(self._public_add_method_name, add_child)
@lazyproperty
def _public_add_method_name(self):
"""
add_childElement() is public API for a repeating element, allowing
new elements to be added to the sequence. May be overridden to
provide a friendlier API to clients having domain appropriate
parameter names for required attributes.
"""
return "add_%s" % self._prop_name
class ZeroOrMore(_BaseChildElement):
"""
Defines an optional repeating child element for MetaOxmlElement.
"""
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""
Add the appropriate methods to *element_cls*.
"""
super(ZeroOrMore, self).populate_class_members(element_cls, prop_name)
self._add_list_getter()
self._add_creator()
self._add_inserter()
self._add_adder()
delattr(element_cls, prop_name)
class ZeroOrOne(_BaseChildElement):
"""Defines an optional child element for MetaOxmlElement."""
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""Add the appropriate methods to `element_cls`."""
super(ZeroOrOne, self).populate_class_members(element_cls, prop_name)
self._add_getter()
self._add_creator()
self._add_inserter()
self._add_adder()
self._add_get_or_adder()
self._add_remover()
def _add_get_or_adder(self):
"""Add a `.get_or_add_x()` method to the element class for this child element."""
def get_or_add_child(obj: BaseOxmlElement) -> BaseOxmlElement:
child = getattr(obj, self._prop_name)
if child is None:
add_method = getattr(obj, self._add_method_name)
child = add_method()
return child
get_or_add_child.__doc__ = (
"Return the ``<%s>`` child element, newly added if not present."
) % self._nsptagname
self._add_to_class(self._get_or_add_method_name, get_or_add_child)
def _add_remover(self):
"""Add a `._remove_x()` method to the element class for this child element."""
def _remove_child(obj: BaseOxmlElement) -> None:
obj.remove_all(self._nsptagname)
_remove_child.__doc__ = f"Remove all `{self._nsptagname}` child elements."
self._add_to_class(self._remove_method_name, _remove_child)
@lazyproperty
def _get_or_add_method_name(self):
return "get_or_add_%s" % self._prop_name
class ZeroOrOneChoice(_BaseChildElement):
"""An `EG_*` element group where at most one of its members may appear as a child."""
def __init__(self, choices: Iterable[Choice], successors: Iterable[str] = ()):
self._choices = tuple(choices)
self._successors = tuple(successors)
def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
"""Add the appropriate methods to `element_cls`."""
super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name)
self._add_choice_getter()
for choice in self._choices:
choice.populate_class_members(element_cls, self._prop_name, self._successors)
self._add_group_remover()
def _add_choice_getter(self):
"""Add a read-only `.{prop_name}` property to the element class.
The property returns the present member of this group, or |None| if none are present.
"""
property_ = property(self._choice_getter, None, None)
# assign unconditionally to overwrite element name definition
setattr(self._element_cls, self._prop_name, property_)
def _add_group_remover(self):
"""Add a `._remove_eg_x()` method to the element class for this choice group."""
def _remove_choice_group(obj: BaseOxmlElement) -> None:
for tagname in self._member_nsptagnames:
obj.remove_all(tagname)
_remove_choice_group.__doc__ = "Remove the current choice group child element if present."
self._add_to_class(self._remove_choice_group_method_name, _remove_choice_group)
@property
def _choice_getter(self):
"""
Return a function object suitable for the "get" side of the property
descriptor.
"""
def get_group_member_element(obj: BaseOxmlElement) -> BaseOxmlElement | None:
return cast(
"BaseOxmlElement | None", obj.first_child_found_in(*self._member_nsptagnames)
)
get_group_member_element.__doc__ = (
"Return the child element belonging to this element group, or "
"|None| if no member child is present."
)
return get_group_member_element
@lazyproperty
def _member_nsptagnames(self) -> list[str]:
"""Sequence of namespace-prefixed tagnames, one for each member element of choice group."""
return [choice.nsptagname for choice in self._choices]
@lazyproperty
def _remove_choice_group_method_name(self):
"""Function-name for choice remover."""
return f"_remove_{self._prop_name}"
# -- lxml typing isn't quite right here, just ignore this error on _Element --
class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement):
"""Effective base class for all custom element classes.
Adds standardized behavior to all classes in one place.
"""
def __repr__(self):
return "<%s '<%s>' at 0x%0x>" % (
self.__class__.__name__,
self._nsptag,
id(self),
)
def first_child_found_in(self, *tagnames: str) -> _Element | None:
"""First child with tag in `tagnames`, or None if not found."""
for tagname in tagnames:
child = self.find(qn(tagname))
if child is not None:
return child
return None
def insert_element_before(self, elm: ElementBase, *tagnames: str):
successor = self.first_child_found_in(*tagnames)
if successor is not None:
successor.addprevious(elm)
else:
self.append(elm)
return elm
def remove_all(self, *tagnames: str) -> None:
"""Remove child elements with tagname (e.g. "a:p") in `tagnames`."""
for tagname in tagnames:
matching = self.findall(qn(tagname))
for child in matching:
self.remove(child)
@property
def xml(self) -> str:
"""XML string for this element, suitable for testing purposes.
Pretty printed for readability and without an XML declaration at the top.
"""
return serialize_for_reading(self)
def xpath(self, xpath_str: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
"""Override of `lxml` _Element.xpath() method.
Provides standard Open XML namespace mapping (`nsmap`) in centralized location.
"""
return super().xpath(xpath_str, namespaces=_nsmap)
@property
def _nsptag(self) -> str:
return NamespacePrefixedTag.from_clark_name(self.tag)