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