diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/oxml')
31 files changed, 8223 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py new file mode 100644 index 00000000..21afaa92 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/action.py b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py new file mode 100644 index 00000000..9b31a9e1 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py new file mode 100644 index 00000000..7129810c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py new file mode 100644 index 00000000..f4cd0dc7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py new file mode 100644 index 00000000..b6aac2fd --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py @@ -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() diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py new file mode 100644 index 00000000..196ca15d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py new file mode 100644 index 00000000..34afd13d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py new file mode 100644 index 00000000..9c695a43 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py new file mode 100644 index 00000000..9264d552 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py new file mode 100644 index 00000000..5515aa4b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py new file mode 100644 index 00000000..de6b26b2 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py new file mode 100644 index 00000000..dfce90aa --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py @@ -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. + """ diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py new file mode 100644 index 00000000..2ff2255d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py @@ -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=(), + ) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py new file mode 100644 index 00000000..720ca8e0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py @@ -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) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py new file mode 100644 index 00000000..d900c33b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py new file mode 100644 index 00000000..17997c2b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py @@ -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 + ) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py new file mode 100644 index 00000000..37f8ef60 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py @@ -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" +) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py new file mode 100644 index 00000000..5d78f624 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py @@ -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") + ) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py new file mode 100644 index 00000000..91261f78 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py new file mode 100644 index 00000000..efa0b363 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py new file mode 100644 index 00000000..f62bc666 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py new file mode 100644 index 00000000..bacc9719 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py @@ -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") diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py new file mode 100644 index 00000000..d9f94569 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py new file mode 100644 index 00000000..6ceb06f7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py new file mode 100644 index 00000000..37a9780f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/table.py b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py new file mode 100644 index 00000000..cd3e9ebc --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py @@ -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 diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/text.py b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py new file mode 100644 index 00000000..0f9ecc15 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py @@ -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 + ) diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py new file mode 100644 index 00000000..19ac8dea --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py @@ -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") diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py new file mode 100644 index 00000000..41fb2e17 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py @@ -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) |
