init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Initialization module for python-pptx package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pptx.exc as exceptions
|
||||
from pptx.api import Presentation
|
||||
from pptx.opc.constants import CONTENT_TYPE as CT
|
||||
from pptx.opc.package import PartFactory
|
||||
from pptx.parts.chart import ChartPart
|
||||
from pptx.parts.coreprops import CorePropertiesPart
|
||||
from pptx.parts.image import ImagePart
|
||||
from pptx.parts.media import MediaPart
|
||||
from pptx.parts.presentation import PresentationPart
|
||||
from pptx.parts.slide import (
|
||||
NotesMasterPart,
|
||||
NotesSlidePart,
|
||||
SlideLayoutPart,
|
||||
SlideMasterPart,
|
||||
SlidePart,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx.opc.package import Part
|
||||
|
||||
__version__ = "1.0.2"
|
||||
|
||||
sys.modules["pptx.exceptions"] = exceptions
|
||||
del sys
|
||||
|
||||
__all__ = ["Presentation"]
|
||||
|
||||
content_type_to_part_class_map: dict[str, type[Part]] = {
|
||||
CT.PML_PRESENTATION_MAIN: PresentationPart,
|
||||
CT.PML_PRES_MACRO_MAIN: PresentationPart,
|
||||
CT.PML_TEMPLATE_MAIN: PresentationPart,
|
||||
CT.PML_SLIDESHOW_MAIN: PresentationPart,
|
||||
CT.OPC_CORE_PROPERTIES: CorePropertiesPart,
|
||||
CT.PML_NOTES_MASTER: NotesMasterPart,
|
||||
CT.PML_NOTES_SLIDE: NotesSlidePart,
|
||||
CT.PML_SLIDE: SlidePart,
|
||||
CT.PML_SLIDE_LAYOUT: SlideLayoutPart,
|
||||
CT.PML_SLIDE_MASTER: SlideMasterPart,
|
||||
CT.DML_CHART: ChartPart,
|
||||
CT.BMP: ImagePart,
|
||||
CT.GIF: ImagePart,
|
||||
CT.JPEG: ImagePart,
|
||||
CT.MS_PHOTO: ImagePart,
|
||||
CT.PNG: ImagePart,
|
||||
CT.TIFF: ImagePart,
|
||||
CT.X_EMF: ImagePart,
|
||||
CT.X_WMF: ImagePart,
|
||||
CT.ASF: MediaPart,
|
||||
CT.AVI: MediaPart,
|
||||
CT.MOV: MediaPart,
|
||||
CT.MP4: MediaPart,
|
||||
CT.MPG: MediaPart,
|
||||
CT.MS_VIDEO: MediaPart,
|
||||
CT.SWF: MediaPart,
|
||||
CT.VIDEO: MediaPart,
|
||||
CT.WMV: MediaPart,
|
||||
CT.X_MS_VIDEO: MediaPart,
|
||||
# -- accommodate "image/jpg" as an alias for "image/jpeg" --
|
||||
"image/jpg": ImagePart,
|
||||
}
|
||||
|
||||
PartFactory.part_type_for.update(content_type_to_part_class_map)
|
||||
|
||||
del (
|
||||
ChartPart,
|
||||
CorePropertiesPart,
|
||||
ImagePart,
|
||||
MediaPart,
|
||||
SlidePart,
|
||||
SlideLayoutPart,
|
||||
SlideMasterPart,
|
||||
PresentationPart,
|
||||
CT,
|
||||
PartFactory,
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,270 @@
|
||||
"""Objects related to mouse click and hover actions on a shape or text."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from pptx.enum.action import PP_ACTION
|
||||
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
|
||||
from pptx.shapes import Subshape
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx.oxml.action import CT_Hyperlink
|
||||
from pptx.oxml.shapes.shared import CT_NonVisualDrawingProps
|
||||
from pptx.oxml.text import CT_TextCharacterProperties
|
||||
from pptx.parts.slide import SlidePart
|
||||
from pptx.shapes.base import BaseShape
|
||||
from pptx.slide import Slide, Slides
|
||||
|
||||
|
||||
class ActionSetting(Subshape):
|
||||
"""Properties specifying how a shape or run reacts to mouse actions."""
|
||||
|
||||
# -- The Subshape base class provides access to the Slide Part, which is needed to access
|
||||
# -- relationships, which is where hyperlinks live.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
|
||||
parent: BaseShape,
|
||||
hover: bool = False,
|
||||
):
|
||||
super(ActionSetting, self).__init__(parent)
|
||||
# xPr is either a cNvPr or rPr element
|
||||
self._element = xPr
|
||||
# _hover determines use of `a:hlinkClick` or `a:hlinkHover`
|
||||
self._hover = hover
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
"""Member of :ref:`PpActionType` enumeration, such as `PP_ACTION.HYPERLINK`.
|
||||
|
||||
The returned member indicates the type of action that will result when the
|
||||
specified shape or text is clicked or the mouse pointer is positioned over the
|
||||
shape during a slide show.
|
||||
|
||||
If there is no click-action or the click-action value is not recognized (is not
|
||||
one of the official `MsoPpAction` values) then `PP_ACTION.NONE` is returned.
|
||||
"""
|
||||
hlink = self._hlink
|
||||
|
||||
if hlink is None:
|
||||
return PP_ACTION.NONE
|
||||
|
||||
action_verb = hlink.action_verb
|
||||
|
||||
if action_verb == "hlinkshowjump":
|
||||
relative_target = hlink.action_fields["jump"]
|
||||
return {
|
||||
"firstslide": PP_ACTION.FIRST_SLIDE,
|
||||
"lastslide": PP_ACTION.LAST_SLIDE,
|
||||
"lastslideviewed": PP_ACTION.LAST_SLIDE_VIEWED,
|
||||
"nextslide": PP_ACTION.NEXT_SLIDE,
|
||||
"previousslide": PP_ACTION.PREVIOUS_SLIDE,
|
||||
"endshow": PP_ACTION.END_SHOW,
|
||||
}[relative_target]
|
||||
|
||||
return {
|
||||
None: PP_ACTION.HYPERLINK,
|
||||
"hlinksldjump": PP_ACTION.NAMED_SLIDE,
|
||||
"hlinkpres": PP_ACTION.PLAY,
|
||||
"hlinkfile": PP_ACTION.OPEN_FILE,
|
||||
"customshow": PP_ACTION.NAMED_SLIDE_SHOW,
|
||||
"ole": PP_ACTION.OLE_VERB,
|
||||
"macro": PP_ACTION.RUN_MACRO,
|
||||
"program": PP_ACTION.RUN_PROGRAM,
|
||||
}.get(action_verb, PP_ACTION.NONE)
|
||||
|
||||
@lazyproperty
|
||||
def hyperlink(self) -> Hyperlink:
|
||||
"""
|
||||
A |Hyperlink| object representing the hyperlink action defined on
|
||||
this click or hover mouse event. A |Hyperlink| object is always
|
||||
returned, even if no hyperlink or other click action is defined.
|
||||
"""
|
||||
return Hyperlink(self._element, self._parent, self._hover)
|
||||
|
||||
@property
|
||||
def target_slide(self) -> Slide | None:
|
||||
"""
|
||||
A reference to the slide in this presentation that is the target of
|
||||
the slide jump action in this shape. Slide jump actions include
|
||||
`PP_ACTION.FIRST_SLIDE`, `LAST_SLIDE`, `NEXT_SLIDE`,
|
||||
`PREVIOUS_SLIDE`, and `NAMED_SLIDE`. Returns |None| for all other
|
||||
actions. In particular, the `LAST_SLIDE_VIEWED` action and the `PLAY`
|
||||
(start other presentation) actions are not supported.
|
||||
|
||||
A slide object may be assigned to this property, which makes the
|
||||
shape an "internal hyperlink" to the assigened slide::
|
||||
|
||||
slide, target_slide = prs.slides[0], prs.slides[1]
|
||||
shape = slide.shapes[0]
|
||||
shape.target_slide = target_slide
|
||||
|
||||
Assigning |None| removes any slide jump action. Note that this is
|
||||
accomplished by removing any action present (such as a hyperlink),
|
||||
without first checking that it is a slide jump action.
|
||||
"""
|
||||
slide_jump_actions = (
|
||||
PP_ACTION.FIRST_SLIDE,
|
||||
PP_ACTION.LAST_SLIDE,
|
||||
PP_ACTION.NEXT_SLIDE,
|
||||
PP_ACTION.PREVIOUS_SLIDE,
|
||||
PP_ACTION.NAMED_SLIDE,
|
||||
)
|
||||
|
||||
if self.action not in slide_jump_actions:
|
||||
return None
|
||||
|
||||
if self.action == PP_ACTION.FIRST_SLIDE:
|
||||
return self._slides[0]
|
||||
elif self.action == PP_ACTION.LAST_SLIDE:
|
||||
return self._slides[-1]
|
||||
elif self.action == PP_ACTION.NEXT_SLIDE:
|
||||
next_slide_idx = self._slide_index + 1
|
||||
if next_slide_idx >= len(self._slides):
|
||||
raise ValueError("no next slide")
|
||||
return self._slides[next_slide_idx]
|
||||
elif self.action == PP_ACTION.PREVIOUS_SLIDE:
|
||||
prev_slide_idx = self._slide_index - 1
|
||||
if prev_slide_idx < 0:
|
||||
raise ValueError("no previous slide")
|
||||
return self._slides[prev_slide_idx]
|
||||
elif self.action == PP_ACTION.NAMED_SLIDE:
|
||||
assert self._hlink is not None
|
||||
rId = self._hlink.rId
|
||||
slide_part = cast("SlidePart", self.part.related_part(rId))
|
||||
return slide_part.slide
|
||||
|
||||
@target_slide.setter
|
||||
def target_slide(self, slide: Slide | None):
|
||||
self._clear_click_action()
|
||||
if slide is None:
|
||||
return
|
||||
hlink = self._element.get_or_add_hlinkClick()
|
||||
hlink.action = "ppaction://hlinksldjump"
|
||||
hlink.rId = self.part.relate_to(slide.part, RT.SLIDE)
|
||||
|
||||
def _clear_click_action(self):
|
||||
"""Remove any existing click action."""
|
||||
hlink = self._hlink
|
||||
if hlink is None:
|
||||
return
|
||||
rId = hlink.rId
|
||||
if rId:
|
||||
self.part.drop_rel(rId)
|
||||
self._element.remove(hlink)
|
||||
|
||||
@property
|
||||
def _hlink(self) -> CT_Hyperlink | None:
|
||||
"""
|
||||
Reference to the `a:hlinkClick` or `a:hlinkHover` element for this
|
||||
click action. Returns |None| if the element is not present.
|
||||
"""
|
||||
if self._hover:
|
||||
assert isinstance(self._element, CT_NonVisualDrawingProps)
|
||||
return self._element.hlinkHover
|
||||
return self._element.hlinkClick
|
||||
|
||||
@lazyproperty
|
||||
def _slide(self):
|
||||
"""
|
||||
Reference to the slide containing the shape having this click action.
|
||||
"""
|
||||
return self.part.slide
|
||||
|
||||
@lazyproperty
|
||||
def _slide_index(self):
|
||||
"""
|
||||
Position in the slide collection of the slide containing the shape
|
||||
having this click action.
|
||||
"""
|
||||
return self._slides.index(self._slide)
|
||||
|
||||
@lazyproperty
|
||||
def _slides(self) -> Slides:
|
||||
"""
|
||||
Reference to the slide collection for this presentation.
|
||||
"""
|
||||
return self.part.package.presentation_part.presentation.slides
|
||||
|
||||
|
||||
class Hyperlink(Subshape):
|
||||
"""Represents a hyperlink action on a shape or text run."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
|
||||
parent: BaseShape,
|
||||
hover: bool = False,
|
||||
):
|
||||
super(Hyperlink, self).__init__(parent)
|
||||
# xPr is either a cNvPr or rPr element
|
||||
self._element = xPr
|
||||
# _hover determines use of `a:hlinkClick` or `a:hlinkHover`
|
||||
self._hover = hover
|
||||
|
||||
@property
|
||||
def address(self) -> str | None:
|
||||
"""Read/write. The URL of the hyperlink.
|
||||
|
||||
URL can be on http, https, mailto, or file scheme; others may work. Returns |None| if no
|
||||
hyperlink is defined, including when another action such as `RUN_MACRO` is defined on the
|
||||
object. Assigning |None| removes any action defined on the object, whether it is a hyperlink
|
||||
action or not.
|
||||
"""
|
||||
hlink = self._hlink
|
||||
|
||||
# there's no URL if there's no click action
|
||||
if hlink is None:
|
||||
return None
|
||||
|
||||
# a click action without a relationship has no URL
|
||||
rId = hlink.rId
|
||||
if not rId:
|
||||
return None
|
||||
|
||||
return self.part.target_ref(rId)
|
||||
|
||||
@address.setter
|
||||
def address(self, url: str | None):
|
||||
# implements all three of add, change, and remove hyperlink
|
||||
self._remove_hlink()
|
||||
|
||||
if url:
|
||||
rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
|
||||
hlink = self._get_or_add_hlink()
|
||||
hlink.rId = rId
|
||||
|
||||
def _get_or_add_hlink(self) -> CT_Hyperlink:
|
||||
"""Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink object.
|
||||
|
||||
The actual element depends on the value of `self._hover`. Create the element if not present.
|
||||
"""
|
||||
if self._hover:
|
||||
return cast("CT_NonVisualDrawingProps", self._element).get_or_add_hlinkHover()
|
||||
return self._element.get_or_add_hlinkClick()
|
||||
|
||||
@property
|
||||
def _hlink(self) -> CT_Hyperlink | None:
|
||||
"""Reference to the `a:hlinkClick` or `h:hlinkHover` element for this click action.
|
||||
|
||||
Returns |None| if the element is not present.
|
||||
"""
|
||||
if self._hover:
|
||||
return cast("CT_NonVisualDrawingProps", self._element).hlinkHover
|
||||
return self._element.hlinkClick
|
||||
|
||||
def _remove_hlink(self):
|
||||
"""Remove the a:hlinkClick or a:hlinkHover element.
|
||||
|
||||
Also drops any relationship it might have.
|
||||
"""
|
||||
hlink = self._hlink
|
||||
if hlink is None:
|
||||
return
|
||||
rId = hlink.rId
|
||||
if rId:
|
||||
self.part.drop_rel(rId)
|
||||
self._element.remove(hlink)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Directly exposed API classes, Presentation for now.
|
||||
|
||||
Provides some syntactic sugar for interacting with the pptx.presentation.Package graph and also
|
||||
provides some insulation so not so many classes in the other modules need to be named as internal
|
||||
(leading underscore).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import IO, TYPE_CHECKING
|
||||
|
||||
from pptx.opc.constants import CONTENT_TYPE as CT
|
||||
from pptx.package import Package
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx import presentation
|
||||
from pptx.parts.presentation import PresentationPart
|
||||
|
||||
|
||||
def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
|
||||
"""
|
||||
Return a |Presentation| object loaded from *pptx*, where *pptx* can be
|
||||
either a path to a ``.pptx`` file (a string) or a file-like object. If
|
||||
*pptx* is missing or ``None``, the built-in default presentation
|
||||
"template" is loaded.
|
||||
"""
|
||||
if pptx is None:
|
||||
pptx = _default_pptx_path()
|
||||
|
||||
presentation_part = Package.open(pptx).main_document_part
|
||||
|
||||
if not _is_pptx_package(presentation_part):
|
||||
tmpl = "file '%s' is not a PowerPoint file, content type is '%s'"
|
||||
raise ValueError(tmpl % (pptx, presentation_part.content_type))
|
||||
|
||||
return presentation_part.presentation
|
||||
|
||||
|
||||
def _default_pptx_path() -> str:
|
||||
"""Return the path to the built-in default .pptx package."""
|
||||
_thisdir = os.path.split(__file__)[0]
|
||||
return os.path.join(_thisdir, "templates", "default.pptx")
|
||||
|
||||
|
||||
def _is_pptx_package(prs_part: PresentationPart):
|
||||
"""Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
|
||||
valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN)
|
||||
return prs_part.content_type in valid_content_types
|
||||
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.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,523 @@
|
||||
"""Axis-related chart objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.dml.chtfmt import ChartFormat
|
||||
from pptx.enum.chart import (
|
||||
XL_AXIS_CROSSES,
|
||||
XL_CATEGORY_TYPE,
|
||||
XL_TICK_LABEL_POSITION,
|
||||
XL_TICK_MARK,
|
||||
)
|
||||
from pptx.oxml.ns import qn
|
||||
from pptx.oxml.simpletypes import ST_Orientation
|
||||
from pptx.shared import ElementProxy
|
||||
from pptx.text.text import Font, TextFrame
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class _BaseAxis(object):
|
||||
"""Base class for chart axis objects. All axis objects share these properties."""
|
||||
|
||||
def __init__(self, xAx):
|
||||
super(_BaseAxis, self).__init__()
|
||||
self._element = xAx # axis element, c:catAx or c:valAx
|
||||
self._xAx = xAx
|
||||
|
||||
@property
|
||||
def axis_title(self):
|
||||
"""An |AxisTitle| object providing access to title properties.
|
||||
|
||||
Calling this property is destructive in the sense that it adds an
|
||||
axis title element (`c:title`) to the axis XML if one is not already
|
||||
present. Use :attr:`has_title` to test for presence of axis title
|
||||
non-destructively.
|
||||
"""
|
||||
return AxisTitle(self._element.get_or_add_title())
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""
|
||||
The |ChartFormat| object providing access to the shape formatting
|
||||
properties of this axis, such as its line color and fill.
|
||||
"""
|
||||
return ChartFormat(self._element)
|
||||
|
||||
@property
|
||||
def has_major_gridlines(self):
|
||||
"""
|
||||
Read/write boolean value specifying whether this axis has gridlines
|
||||
at its major tick mark locations. Assigning |True| to this property
|
||||
causes major gridlines to be displayed. Assigning |False| causes them
|
||||
to be removed.
|
||||
"""
|
||||
if self._element.majorGridlines is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_major_gridlines.setter
|
||||
def has_major_gridlines(self, value):
|
||||
if bool(value) is True:
|
||||
self._element.get_or_add_majorGridlines()
|
||||
else:
|
||||
self._element._remove_majorGridlines()
|
||||
|
||||
@property
|
||||
def has_minor_gridlines(self):
|
||||
"""
|
||||
Read/write boolean value specifying whether this axis has gridlines
|
||||
at its minor tick mark locations. Assigning |True| to this property
|
||||
causes minor gridlines to be displayed. Assigning |False| causes them
|
||||
to be removed.
|
||||
"""
|
||||
if self._element.minorGridlines is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_minor_gridlines.setter
|
||||
def has_minor_gridlines(self, value):
|
||||
if bool(value) is True:
|
||||
self._element.get_or_add_minorGridlines()
|
||||
else:
|
||||
self._element._remove_minorGridlines()
|
||||
|
||||
@property
|
||||
def has_title(self):
|
||||
"""Read/write boolean specifying whether this axis has a title.
|
||||
|
||||
|True| if this axis has a title, |False| otherwise. Assigning |True|
|
||||
causes an axis title to be added if not already present. Assigning
|
||||
|False| causes any existing title to be deleted.
|
||||
"""
|
||||
if self._element.title is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_title.setter
|
||||
def has_title(self, value):
|
||||
if bool(value) is True:
|
||||
self._element.get_or_add_title()
|
||||
else:
|
||||
self._element._remove_title()
|
||||
|
||||
@lazyproperty
|
||||
def major_gridlines(self):
|
||||
"""
|
||||
The |MajorGridlines| object representing the major gridlines for
|
||||
this axis.
|
||||
"""
|
||||
return MajorGridlines(self._element)
|
||||
|
||||
@property
|
||||
def major_tick_mark(self):
|
||||
"""
|
||||
Read/write :ref:`XlTickMark` value specifying the type of major tick
|
||||
mark to display on this axis.
|
||||
"""
|
||||
majorTickMark = self._element.majorTickMark
|
||||
if majorTickMark is None:
|
||||
return XL_TICK_MARK.CROSS
|
||||
return majorTickMark.val
|
||||
|
||||
@major_tick_mark.setter
|
||||
def major_tick_mark(self, value):
|
||||
self._element._remove_majorTickMark()
|
||||
if value is XL_TICK_MARK.CROSS:
|
||||
return
|
||||
self._element._add_majorTickMark(val=value)
|
||||
|
||||
@property
|
||||
def maximum_scale(self):
|
||||
"""
|
||||
Read/write float value specifying the upper limit of the value range
|
||||
for this axis, the number at the top or right of the vertical or
|
||||
horizontal value scale, respectively. The value |None| indicates the
|
||||
upper limit should be determined automatically based on the range of
|
||||
data point values associated with the axis.
|
||||
"""
|
||||
return self._element.scaling.maximum
|
||||
|
||||
@maximum_scale.setter
|
||||
def maximum_scale(self, value):
|
||||
scaling = self._element.scaling
|
||||
scaling.maximum = value
|
||||
|
||||
@property
|
||||
def minimum_scale(self):
|
||||
"""
|
||||
Read/write float value specifying lower limit of value range, the
|
||||
number at the bottom or left of the value scale. |None| if no minimum
|
||||
scale has been set. The value |None| indicates the lower limit should
|
||||
be determined automatically based on the range of data point values
|
||||
associated with the axis.
|
||||
"""
|
||||
return self._element.scaling.minimum
|
||||
|
||||
@minimum_scale.setter
|
||||
def minimum_scale(self, value):
|
||||
scaling = self._element.scaling
|
||||
scaling.minimum = value
|
||||
|
||||
@property
|
||||
def minor_tick_mark(self):
|
||||
"""
|
||||
Read/write :ref:`XlTickMark` value specifying the type of minor tick
|
||||
mark for this axis.
|
||||
"""
|
||||
minorTickMark = self._element.minorTickMark
|
||||
if minorTickMark is None:
|
||||
return XL_TICK_MARK.CROSS
|
||||
return minorTickMark.val
|
||||
|
||||
@minor_tick_mark.setter
|
||||
def minor_tick_mark(self, value):
|
||||
self._element._remove_minorTickMark()
|
||||
if value is XL_TICK_MARK.CROSS:
|
||||
return
|
||||
self._element._add_minorTickMark(val=value)
|
||||
|
||||
@property
|
||||
def reverse_order(self):
|
||||
"""Read/write bool value specifying whether to reverse plotting order for axis.
|
||||
|
||||
For a category axis, this reverses the order in which the categories are
|
||||
displayed. This may be desired, for example, on a (horizontal) bar-chart where
|
||||
by default the first category appears at the bottom. Since we read from
|
||||
top-to-bottom, many viewers may find it most natural for the first category to
|
||||
appear on top.
|
||||
|
||||
For a value axis, it reverses the direction of increasing value from
|
||||
bottom-to-top to top-to-bottom.
|
||||
"""
|
||||
return self._element.orientation == ST_Orientation.MAX_MIN
|
||||
|
||||
@reverse_order.setter
|
||||
def reverse_order(self, value):
|
||||
self._element.orientation = (
|
||||
ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX
|
||||
)
|
||||
|
||||
@lazyproperty
|
||||
def tick_labels(self):
|
||||
"""
|
||||
The |TickLabels| instance providing access to axis tick label
|
||||
formatting properties. Tick labels are the numbers appearing on
|
||||
a value axis or the category names appearing on a category axis.
|
||||
"""
|
||||
return TickLabels(self._element)
|
||||
|
||||
@property
|
||||
def tick_label_position(self):
|
||||
"""
|
||||
Read/write :ref:`XlTickLabelPosition` value specifying where the tick
|
||||
labels for this axis should appear.
|
||||
"""
|
||||
tickLblPos = self._element.tickLblPos
|
||||
if tickLblPos is None:
|
||||
return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
|
||||
if tickLblPos.val is None:
|
||||
return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
|
||||
return tickLblPos.val
|
||||
|
||||
@tick_label_position.setter
|
||||
def tick_label_position(self, value):
|
||||
tickLblPos = self._element.get_or_add_tickLblPos()
|
||||
tickLblPos.val = value
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Read/write. |True| if axis is visible, |False| otherwise.
|
||||
"""
|
||||
delete = self._element.delete_
|
||||
if delete is None:
|
||||
return False
|
||||
return False if delete.val else True
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value):
|
||||
if value not in (True, False):
|
||||
raise ValueError("assigned value must be True or False, got: %s" % value)
|
||||
delete = self._element.get_or_add_delete_()
|
||||
delete.val = not value
|
||||
|
||||
|
||||
class AxisTitle(ElementProxy):
|
||||
"""Provides properties for manipulating axis title."""
|
||||
|
||||
def __init__(self, title):
|
||||
super(AxisTitle, self).__init__(title)
|
||||
self._title = title
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""|ChartFormat| object providing access to shape formatting.
|
||||
|
||||
Return the |ChartFormat| object providing shape formatting properties
|
||||
for this axis title, such as its line color and fill.
|
||||
"""
|
||||
return ChartFormat(self._element)
|
||||
|
||||
@property
|
||||
def has_text_frame(self):
|
||||
"""Read/write Boolean specifying presence of a text frame.
|
||||
|
||||
Return |True| if this axis title has a text frame, and |False|
|
||||
otherwise. Assigning |True| causes a text frame to be added if not
|
||||
already present. Assigning |False| causes any existing text frame to
|
||||
be removed along with any text contained in the text frame.
|
||||
"""
|
||||
if self._title.tx_rich is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_text_frame.setter
|
||||
def has_text_frame(self, value):
|
||||
if bool(value) is True:
|
||||
self._title.get_or_add_tx_rich()
|
||||
else:
|
||||
self._title._remove_tx()
|
||||
|
||||
@property
|
||||
def text_frame(self):
|
||||
"""|TextFrame| instance for this axis title.
|
||||
|
||||
Return a |TextFrame| instance allowing read/write access to the text
|
||||
of this axis title and its text formatting properties. Accessing this
|
||||
property is destructive as it adds a new text frame if not already
|
||||
present.
|
||||
"""
|
||||
rich = self._title.get_or_add_tx_rich()
|
||||
return TextFrame(rich, self)
|
||||
|
||||
|
||||
class CategoryAxis(_BaseAxis):
|
||||
"""A category axis of a chart."""
|
||||
|
||||
@property
|
||||
def category_type(self):
|
||||
"""
|
||||
A member of :ref:`XlCategoryType` specifying the scale type of this
|
||||
axis. Unconditionally ``CATEGORY_SCALE`` for a |CategoryAxis| object.
|
||||
"""
|
||||
return XL_CATEGORY_TYPE.CATEGORY_SCALE
|
||||
|
||||
|
||||
class DateAxis(_BaseAxis):
|
||||
"""A category axis with dates as its category labels.
|
||||
|
||||
This axis-type has some special display behaviors such as making length of equal
|
||||
periods equal and normalizing month start dates despite unequal month lengths.
|
||||
"""
|
||||
|
||||
@property
|
||||
def category_type(self):
|
||||
"""
|
||||
A member of :ref:`XlCategoryType` specifying the scale type of this
|
||||
axis. Unconditionally ``TIME_SCALE`` for a |DateAxis| object.
|
||||
"""
|
||||
return XL_CATEGORY_TYPE.TIME_SCALE
|
||||
|
||||
|
||||
class MajorGridlines(ElementProxy):
|
||||
"""Provides access to the properties of the major gridlines appearing on an axis."""
|
||||
|
||||
def __init__(self, xAx):
|
||||
super(MajorGridlines, self).__init__(xAx)
|
||||
self._xAx = xAx # axis element, catAx or valAx
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""
|
||||
The |ChartFormat| object providing access to the shape formatting
|
||||
properties of this data point, such as line and fill.
|
||||
"""
|
||||
majorGridlines = self._xAx.get_or_add_majorGridlines()
|
||||
return ChartFormat(majorGridlines)
|
||||
|
||||
|
||||
class TickLabels(object):
|
||||
"""A service class providing access to formatting of axis tick mark labels."""
|
||||
|
||||
def __init__(self, xAx_elm):
|
||||
super(TickLabels, self).__init__()
|
||||
self._element = xAx_elm
|
||||
|
||||
@lazyproperty
|
||||
def font(self):
|
||||
"""
|
||||
The |Font| object that provides access to the text properties for
|
||||
these tick labels, such as bold, italic, etc.
|
||||
"""
|
||||
defRPr = self._element.defRPr
|
||||
font = Font(defRPr)
|
||||
return font
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
Read/write string (e.g. "$#,##0.00") specifying the format for the
|
||||
numbers on this axis. The syntax for these strings is the same as it
|
||||
appears in the PowerPoint or Excel UI. Returns 'General' if no number
|
||||
format has been set. Note that this format string has no effect on
|
||||
rendered tick labels when :meth:`number_format_is_linked` is |True|.
|
||||
Assigning a format string to this property automatically sets
|
||||
:meth:`number_format_is_linked` to |False|.
|
||||
"""
|
||||
numFmt = self._element.numFmt
|
||||
if numFmt is None:
|
||||
return "General"
|
||||
return numFmt.formatCode
|
||||
|
||||
@number_format.setter
|
||||
def number_format(self, value):
|
||||
numFmt = self._element.get_or_add_numFmt()
|
||||
numFmt.formatCode = value
|
||||
self.number_format_is_linked = False
|
||||
|
||||
@property
|
||||
def number_format_is_linked(self):
|
||||
"""
|
||||
Read/write boolean specifying whether number formatting should be
|
||||
taken from the source spreadsheet rather than the value of
|
||||
:meth:`number_format`.
|
||||
"""
|
||||
numFmt = self._element.numFmt
|
||||
if numFmt is None:
|
||||
return False
|
||||
souceLinked = numFmt.sourceLinked
|
||||
if souceLinked is None:
|
||||
return True
|
||||
return numFmt.sourceLinked
|
||||
|
||||
@number_format_is_linked.setter
|
||||
def number_format_is_linked(self, value):
|
||||
numFmt = self._element.get_or_add_numFmt()
|
||||
numFmt.sourceLinked = value
|
||||
|
||||
@property
|
||||
def offset(self):
|
||||
"""
|
||||
Read/write int value in range 0-1000 specifying the spacing between
|
||||
the tick mark labels and the axis as a percentange of the default
|
||||
value. 100 if no label offset setting is present.
|
||||
"""
|
||||
lblOffset = self._element.lblOffset
|
||||
if lblOffset is None:
|
||||
return 100
|
||||
return lblOffset.val
|
||||
|
||||
@offset.setter
|
||||
def offset(self, value):
|
||||
if self._element.tag != qn("c:catAx"):
|
||||
raise ValueError("only a category axis has an offset")
|
||||
self._element._remove_lblOffset()
|
||||
if value == 100:
|
||||
return
|
||||
lblOffset = self._element._add_lblOffset()
|
||||
lblOffset.val = value
|
||||
|
||||
|
||||
class ValueAxis(_BaseAxis):
|
||||
"""An axis having continuous (as opposed to discrete) values.
|
||||
|
||||
The vertical axis is generally a value axis, however both axes of an XY-type chart
|
||||
are value axes.
|
||||
"""
|
||||
|
||||
@property
|
||||
def crosses(self):
|
||||
"""
|
||||
Member of :ref:`XlAxisCrosses` enumeration specifying the point on
|
||||
this axis where the other axis crosses, such as auto/zero, minimum,
|
||||
or maximum. Returns `XL_AXIS_CROSSES.CUSTOM` when a specific numeric
|
||||
crossing point (e.g. 1.5) is defined.
|
||||
"""
|
||||
crosses = self._cross_xAx.crosses
|
||||
if crosses is None:
|
||||
return XL_AXIS_CROSSES.CUSTOM
|
||||
return crosses.val
|
||||
|
||||
@crosses.setter
|
||||
def crosses(self, value):
|
||||
cross_xAx = self._cross_xAx
|
||||
if value == XL_AXIS_CROSSES.CUSTOM:
|
||||
if cross_xAx.crossesAt is not None:
|
||||
return
|
||||
cross_xAx._remove_crosses()
|
||||
cross_xAx._remove_crossesAt()
|
||||
if value == XL_AXIS_CROSSES.CUSTOM:
|
||||
cross_xAx._add_crossesAt(val=0.0)
|
||||
else:
|
||||
cross_xAx._add_crosses(val=value)
|
||||
|
||||
@property
|
||||
def crosses_at(self):
|
||||
"""
|
||||
Numeric value on this axis at which the perpendicular axis crosses.
|
||||
Returns |None| if no crossing value is set.
|
||||
"""
|
||||
crossesAt = self._cross_xAx.crossesAt
|
||||
if crossesAt is None:
|
||||
return None
|
||||
return crossesAt.val
|
||||
|
||||
@crosses_at.setter
|
||||
def crosses_at(self, value):
|
||||
cross_xAx = self._cross_xAx
|
||||
cross_xAx._remove_crosses()
|
||||
cross_xAx._remove_crossesAt()
|
||||
if value is None:
|
||||
return
|
||||
cross_xAx._add_crossesAt(val=value)
|
||||
|
||||
@property
|
||||
def major_unit(self):
|
||||
"""
|
||||
The float number of units between major tick marks on this value
|
||||
axis. |None| corresponds to the 'Auto' setting in the UI, and
|
||||
specifies the value should be calculated by PowerPoint based on the
|
||||
underlying chart data.
|
||||
"""
|
||||
majorUnit = self._element.majorUnit
|
||||
if majorUnit is None:
|
||||
return None
|
||||
return majorUnit.val
|
||||
|
||||
@major_unit.setter
|
||||
def major_unit(self, value):
|
||||
self._element._remove_majorUnit()
|
||||
if value is None:
|
||||
return
|
||||
self._element._add_majorUnit(val=value)
|
||||
|
||||
@property
|
||||
def minor_unit(self):
|
||||
"""
|
||||
The float number of units between minor tick marks on this value
|
||||
axis. |None| corresponds to the 'Auto' setting in the UI, and
|
||||
specifies the value should be calculated by PowerPoint based on the
|
||||
underlying chart data.
|
||||
"""
|
||||
minorUnit = self._element.minorUnit
|
||||
if minorUnit is None:
|
||||
return None
|
||||
return minorUnit.val
|
||||
|
||||
@minor_unit.setter
|
||||
def minor_unit(self, value):
|
||||
self._element._remove_minorUnit()
|
||||
if value is None:
|
||||
return
|
||||
self._element._add_minorUnit(val=value)
|
||||
|
||||
@property
|
||||
def _cross_xAx(self):
|
||||
"""
|
||||
The axis element in the same group (primary/secondary) that crosses
|
||||
this axis.
|
||||
"""
|
||||
crossAx_id = self._element.crossAx.val
|
||||
expr = '(../c:catAx | ../c:valAx | ../c:dateAx)/c:axId[@val="%d"]' % crossAx_id
|
||||
cross_axId = self._element.xpath(expr)[0]
|
||||
return cross_axId.getparent()
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Category-related objects.
|
||||
|
||||
The |category.Categories| object is returned by ``Plot.categories`` and contains zero or
|
||||
more |category.Category| objects, each representing one of the category labels
|
||||
associated with the plot. Categories can be hierarchical, so there are members allowing
|
||||
discovery of the depth of that hierarchy and providing means to navigate it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
class Categories(Sequence):
|
||||
"""
|
||||
A sequence of |category.Category| objects, each representing a category
|
||||
label on the chart. Provides properties for dealing with hierarchical
|
||||
categories.
|
||||
"""
|
||||
|
||||
def __init__(self, xChart):
|
||||
super(Categories, self).__init__()
|
||||
self._xChart = xChart
|
||||
|
||||
def __getitem__(self, idx):
|
||||
pt = self._xChart.cat_pts[idx]
|
||||
return Category(pt, idx)
|
||||
|
||||
def __iter__(self):
|
||||
cat_pts = self._xChart.cat_pts
|
||||
for idx, pt in enumerate(cat_pts):
|
||||
yield Category(pt, idx)
|
||||
|
||||
def __len__(self):
|
||||
# a category can be "null", meaning the Excel cell for it is empty.
|
||||
# In this case, there is no c:pt element for it. The "empty" category
|
||||
# will, however, be accounted for in c:cat//c:ptCount/@val, which
|
||||
# reflects the true length of the categories collection.
|
||||
return self._xChart.cat_pt_count
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
"""
|
||||
Return an integer representing the number of hierarchical levels in
|
||||
this category collection. Returns 1 for non-hierarchical categories
|
||||
and 0 if no categories are present (generally meaning no series are
|
||||
present).
|
||||
"""
|
||||
cat = self._xChart.cat
|
||||
if cat is None:
|
||||
return 0
|
||||
if cat.multiLvlStrRef is None:
|
||||
return 1
|
||||
return len(cat.lvls)
|
||||
|
||||
@property
|
||||
def flattened_labels(self):
|
||||
"""
|
||||
Return a sequence of tuples, each containing the flattened hierarchy
|
||||
of category labels for a leaf category. Each tuple is in parent ->
|
||||
child order, e.g. ``('US', 'CA', 'San Francisco')``, with the leaf
|
||||
category appearing last. If this categories collection is
|
||||
non-hierarchical, each tuple will contain only a leaf category label.
|
||||
If the plot has no series (and therefore no categories), an empty
|
||||
tuple is returned.
|
||||
"""
|
||||
cat = self._xChart.cat
|
||||
if cat is None:
|
||||
return ()
|
||||
|
||||
if cat.multiLvlStrRef is None:
|
||||
return tuple([(category.label,) for category in self])
|
||||
|
||||
return tuple(
|
||||
[
|
||||
tuple([category.label for category in reversed(flat_cat)])
|
||||
for flat_cat in self._iter_flattened_categories()
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def levels(self):
|
||||
"""
|
||||
Return a sequence of |CategoryLevel| objects representing the
|
||||
hierarchy of this category collection. The sequence is empty when the
|
||||
category collection is not hierarchical, that is, contains only
|
||||
leaf-level categories. The levels are ordered from the leaf level to
|
||||
the root level; so the first level will contain the same categories
|
||||
as this category collection.
|
||||
"""
|
||||
cat = self._xChart.cat
|
||||
if cat is None:
|
||||
return []
|
||||
return [CategoryLevel(lvl) for lvl in cat.lvls]
|
||||
|
||||
def _iter_flattened_categories(self):
|
||||
"""
|
||||
Generate a ``tuple`` object for each leaf category in this
|
||||
collection, containing the leaf category followed by its "parent"
|
||||
categories, e.g. ``('San Francisco', 'CA', 'USA'). Each tuple will be
|
||||
the same length as the number of levels (excepting certain edge
|
||||
cases which I believe always indicate a chart construction error).
|
||||
"""
|
||||
levels = self.levels
|
||||
if not levels:
|
||||
return
|
||||
leaf_level, remaining_levels = levels[0], levels[1:]
|
||||
for category in leaf_level:
|
||||
yield self._parentage((category,), remaining_levels)
|
||||
|
||||
def _parentage(self, categories, levels):
|
||||
"""
|
||||
Return a tuple formed by recursively concatenating *categories* with
|
||||
its next ancestor from *levels*. The idx value of the first category
|
||||
in *categories* determines parentage in all levels. The returned
|
||||
sequence is in child -> parent order. A parent category is the
|
||||
Category object in a next level having the maximum idx value not
|
||||
exceeding that of the leaf category.
|
||||
"""
|
||||
# exhausting levels is the expected recursion termination condition
|
||||
if not levels:
|
||||
return tuple(categories)
|
||||
|
||||
# guard against edge case where next level is present but empty. That
|
||||
# situation is not prohibited for some reason.
|
||||
if not levels[0]:
|
||||
return tuple(categories)
|
||||
|
||||
parent_level, remaining_levels = levels[0], levels[1:]
|
||||
leaf_node = categories[0]
|
||||
|
||||
# Make the first parent the default. A possible edge case is where no
|
||||
# parent is defined for one or more leading values, e.g. idx > 0 for
|
||||
# the first parent.
|
||||
parent = parent_level[0]
|
||||
for category in parent_level:
|
||||
if category.idx > leaf_node.idx:
|
||||
break
|
||||
parent = category
|
||||
|
||||
extended_categories = tuple(categories) + (parent,)
|
||||
return self._parentage(extended_categories, remaining_levels)
|
||||
|
||||
|
||||
class Category(str):
|
||||
"""
|
||||
An extension of `str` that provides the category label as its string
|
||||
value, and additional attributes representing other aspects of the
|
||||
category.
|
||||
"""
|
||||
|
||||
def __new__(cls, pt, *args):
|
||||
category_label = "" if pt is None else pt.v.text
|
||||
return str.__new__(cls, category_label)
|
||||
|
||||
def __init__(self, pt, idx=None):
|
||||
"""
|
||||
*idx* is a required attribute of a c:pt element, but must be
|
||||
specified when pt is None, as when a "placeholder" category is
|
||||
created to represent a missing c:pt element.
|
||||
"""
|
||||
self._element = self._pt = pt
|
||||
self._idx = idx
|
||||
|
||||
@property
|
||||
def idx(self):
|
||||
"""
|
||||
Return an integer representing the index reference of this category.
|
||||
For a leaf node, the index identifies the category. For a parent (or
|
||||
other ancestor) category, the index specifies the first leaf category
|
||||
that ancestor encloses.
|
||||
"""
|
||||
if self._pt is None:
|
||||
return self._idx
|
||||
return self._pt.idx
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
"""
|
||||
Return the label of this category as a string.
|
||||
"""
|
||||
return str(self)
|
||||
|
||||
|
||||
class CategoryLevel(Sequence):
|
||||
"""
|
||||
A sequence of |category.Category| objects representing a single level in
|
||||
a hierarchical category collection. This object is only used when the
|
||||
categories are hierarchical, meaning they have more than one level and
|
||||
higher level categories group those at lower levels.
|
||||
"""
|
||||
|
||||
def __init__(self, lvl):
|
||||
self._element = self._lvl = lvl
|
||||
|
||||
def __getitem__(self, offset):
|
||||
return Category(self._lvl.pt_lst[offset])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._lvl.pt_lst)
|
||||
@@ -0,0 +1,280 @@
|
||||
"""Chart-related objects such as Chart and ChartTitle."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis
|
||||
from pptx.chart.legend import Legend
|
||||
from pptx.chart.plot import PlotFactory, PlotTypeInspector
|
||||
from pptx.chart.series import SeriesCollection
|
||||
from pptx.chart.xmlwriter import SeriesXmlRewriterFactory
|
||||
from pptx.dml.chtfmt import ChartFormat
|
||||
from pptx.shared import ElementProxy, PartElementProxy
|
||||
from pptx.text.text import Font, TextFrame
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class Chart(PartElementProxy):
|
||||
"""A chart object."""
|
||||
|
||||
def __init__(self, chartSpace, chart_part):
|
||||
super(Chart, self).__init__(chartSpace, chart_part)
|
||||
self._chartSpace = chartSpace
|
||||
|
||||
@property
|
||||
def category_axis(self):
|
||||
"""
|
||||
The category axis of this chart. In the case of an XY or Bubble
|
||||
chart, this is the X axis. Raises |ValueError| if no category
|
||||
axis is defined (as is the case for a pie chart, for example).
|
||||
"""
|
||||
catAx_lst = self._chartSpace.catAx_lst
|
||||
if catAx_lst:
|
||||
return CategoryAxis(catAx_lst[0])
|
||||
|
||||
dateAx_lst = self._chartSpace.dateAx_lst
|
||||
if dateAx_lst:
|
||||
return DateAxis(dateAx_lst[0])
|
||||
|
||||
valAx_lst = self._chartSpace.valAx_lst
|
||||
if valAx_lst:
|
||||
return ValueAxis(valAx_lst[0])
|
||||
|
||||
raise ValueError("chart has no category axis")
|
||||
|
||||
@property
|
||||
def chart_style(self):
|
||||
"""
|
||||
Read/write integer index of chart style used to format this chart.
|
||||
Range is from 1 to 48. Value is |None| if no explicit style has been
|
||||
assigned, in which case the default chart style is used. Assigning
|
||||
|None| causes any explicit setting to be removed. The integer index
|
||||
corresponds to the style's position in the chart style gallery in the
|
||||
PowerPoint UI.
|
||||
"""
|
||||
style = self._chartSpace.style
|
||||
if style is None:
|
||||
return None
|
||||
return style.val
|
||||
|
||||
@chart_style.setter
|
||||
def chart_style(self, value):
|
||||
self._chartSpace._remove_style()
|
||||
if value is None:
|
||||
return
|
||||
self._chartSpace._add_style(val=value)
|
||||
|
||||
@property
|
||||
def chart_title(self):
|
||||
"""A |ChartTitle| object providing access to title properties.
|
||||
|
||||
Calling this property is destructive in the sense it adds a chart
|
||||
title element (`c:title`) to the chart XML if one is not already
|
||||
present. Use :attr:`has_title` to test for presence of a chart title
|
||||
non-destructively.
|
||||
"""
|
||||
return ChartTitle(self._element.get_or_add_title())
|
||||
|
||||
@property
|
||||
def chart_type(self):
|
||||
"""Member of :ref:`XlChartType` enumeration specifying type of this chart.
|
||||
|
||||
If the chart has two plots, for example, a line plot overlayed on a bar plot,
|
||||
the type reported is for the first (back-most) plot. Read-only.
|
||||
"""
|
||||
first_plot = self.plots[0]
|
||||
return PlotTypeInspector.chart_type(first_plot)
|
||||
|
||||
@lazyproperty
|
||||
def font(self):
|
||||
"""Font object controlling text format defaults for this chart."""
|
||||
defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr()
|
||||
return Font(defRPr)
|
||||
|
||||
@property
|
||||
def has_legend(self):
|
||||
"""
|
||||
Read/write boolean, |True| if the chart has a legend. Assigning
|
||||
|True| causes a legend to be added to the chart if it doesn't already
|
||||
have one. Assigning False removes any existing legend definition
|
||||
along with any existing legend settings.
|
||||
"""
|
||||
return self._chartSpace.chart.has_legend
|
||||
|
||||
@has_legend.setter
|
||||
def has_legend(self, value):
|
||||
self._chartSpace.chart.has_legend = bool(value)
|
||||
|
||||
@property
|
||||
def has_title(self):
|
||||
"""Read/write boolean, specifying whether this chart has a title.
|
||||
|
||||
Assigning |True| causes a title to be added if not already present.
|
||||
Assigning |False| removes any existing title along with its text and
|
||||
settings.
|
||||
"""
|
||||
title = self._chartSpace.chart.title
|
||||
if title is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_title.setter
|
||||
def has_title(self, value):
|
||||
chart = self._chartSpace.chart
|
||||
if bool(value) is False:
|
||||
chart._remove_title()
|
||||
autoTitleDeleted = chart.get_or_add_autoTitleDeleted()
|
||||
autoTitleDeleted.val = True
|
||||
return
|
||||
chart.get_or_add_title()
|
||||
|
||||
@property
|
||||
def legend(self):
|
||||
"""
|
||||
A |Legend| object providing access to the properties of the legend
|
||||
for this chart.
|
||||
"""
|
||||
legend_elm = self._chartSpace.chart.legend
|
||||
if legend_elm is None:
|
||||
return None
|
||||
return Legend(legend_elm)
|
||||
|
||||
@lazyproperty
|
||||
def plots(self):
|
||||
"""
|
||||
The sequence of plots in this chart. A plot, called a *chart group*
|
||||
in the Microsoft API, is a distinct sequence of one or more series
|
||||
depicted in a particular charting type. For example, a chart having
|
||||
a series plotted as a line overlaid on three series plotted as
|
||||
columns would have two plots; the first corresponding to the three
|
||||
column series and the second to the line series. Plots are sequenced
|
||||
in the order drawn, i.e. back-most to front-most. Supports *len()*,
|
||||
membership (e.g. ``p in plots``), iteration, slicing, and indexed
|
||||
access (e.g. ``plot = plots[i]``).
|
||||
"""
|
||||
plotArea = self._chartSpace.chart.plotArea
|
||||
return _Plots(plotArea, self)
|
||||
|
||||
def replace_data(self, chart_data):
|
||||
"""
|
||||
Use the categories and series values in the |ChartData| object
|
||||
*chart_data* to replace those in the XML and Excel worksheet for this
|
||||
chart.
|
||||
"""
|
||||
rewriter = SeriesXmlRewriterFactory(self.chart_type, chart_data)
|
||||
rewriter.replace_series_data(self._chartSpace)
|
||||
self._workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
|
||||
|
||||
@lazyproperty
|
||||
def series(self):
|
||||
"""
|
||||
A |SeriesCollection| object containing all the series in this
|
||||
chart. When the chart has multiple plots, all the series for the
|
||||
first plot appear before all those for the second, and so on. Series
|
||||
within a plot have an explicit ordering and appear in that sequence.
|
||||
"""
|
||||
return SeriesCollection(self._chartSpace.plotArea)
|
||||
|
||||
@property
|
||||
def value_axis(self):
|
||||
"""
|
||||
The |ValueAxis| object providing access to properties of the value
|
||||
axis of this chart. Raises |ValueError| if the chart has no value
|
||||
axis.
|
||||
"""
|
||||
valAx_lst = self._chartSpace.valAx_lst
|
||||
if not valAx_lst:
|
||||
raise ValueError("chart has no value axis")
|
||||
|
||||
idx = 1 if len(valAx_lst) > 1 else 0
|
||||
return ValueAxis(valAx_lst[idx])
|
||||
|
||||
@property
|
||||
def _workbook(self):
|
||||
"""
|
||||
The |ChartWorkbook| object providing access to the Excel source data
|
||||
for this chart.
|
||||
"""
|
||||
return self.part.chart_workbook
|
||||
|
||||
|
||||
class ChartTitle(ElementProxy):
|
||||
"""Provides properties for manipulating a chart title."""
|
||||
|
||||
# This shares functionality with AxisTitle, which could be factored out
|
||||
# into a base class, perhaps pptx.chart.shared.BaseTitle. I suspect they
|
||||
# actually differ in certain fuller behaviors, but at present they're
|
||||
# essentially identical.
|
||||
|
||||
def __init__(self, title):
|
||||
super(ChartTitle, self).__init__(title)
|
||||
self._title = title
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""|ChartFormat| object providing access to line and fill formatting.
|
||||
|
||||
Return the |ChartFormat| object providing shape formatting properties
|
||||
for this chart title, such as its line color and fill.
|
||||
"""
|
||||
return ChartFormat(self._title)
|
||||
|
||||
@property
|
||||
def has_text_frame(self):
|
||||
"""Read/write Boolean specifying whether this title has a text frame.
|
||||
|
||||
Return |True| if this chart title has a text frame, and |False|
|
||||
otherwise. Assigning |True| causes a text frame to be added if not
|
||||
already present. Assigning |False| causes any existing text frame to
|
||||
be removed along with its text and formatting.
|
||||
"""
|
||||
if self._title.tx_rich is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@has_text_frame.setter
|
||||
def has_text_frame(self, value):
|
||||
if bool(value) is False:
|
||||
self._title._remove_tx()
|
||||
return
|
||||
self._title.get_or_add_tx_rich()
|
||||
|
||||
@property
|
||||
def text_frame(self):
|
||||
"""|TextFrame| instance for this chart title.
|
||||
|
||||
Return a |TextFrame| instance allowing read/write access to the text
|
||||
of this chart title and its text formatting properties. Accessing this
|
||||
property is destructive in the sense it adds a text frame if one is
|
||||
not present. Use :attr:`has_text_frame` to test for the presence of
|
||||
a text frame non-destructively.
|
||||
"""
|
||||
rich = self._title.get_or_add_tx_rich()
|
||||
return TextFrame(rich, self)
|
||||
|
||||
|
||||
class _Plots(Sequence):
|
||||
"""
|
||||
The sequence of plots in a chart, such as a bar plot or a line plot. Most
|
||||
charts have only a single plot. The concept is necessary when two chart
|
||||
types are displayed in a single set of axes, like a bar plot with
|
||||
a superimposed line plot.
|
||||
"""
|
||||
|
||||
def __init__(self, plotArea, chart):
|
||||
super(_Plots, self).__init__()
|
||||
self._plotArea = plotArea
|
||||
self._chart = chart
|
||||
|
||||
def __getitem__(self, index):
|
||||
xCharts = self._plotArea.xCharts
|
||||
if isinstance(index, slice):
|
||||
plots = [PlotFactory(xChart, self._chart) for xChart in xCharts]
|
||||
return plots[index]
|
||||
else:
|
||||
xChart = xCharts[index]
|
||||
return PlotFactory(xChart, self._chart)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._plotArea.xCharts)
|
||||
@@ -0,0 +1,864 @@
|
||||
"""ChartData and related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from collections.abc import Sequence
|
||||
from numbers import Number
|
||||
|
||||
from pptx.chart.xlsx import (
|
||||
BubbleWorkbookWriter,
|
||||
CategoryWorkbookWriter,
|
||||
XyWorkbookWriter,
|
||||
)
|
||||
from pptx.chart.xmlwriter import ChartXmlWriter
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class _BaseChartData(Sequence):
|
||||
"""Base class providing common members for chart data objects.
|
||||
|
||||
A chart data object serves as a proxy for the chart data table that will be written to an
|
||||
Excel worksheet; operating as a sequence of series as well as providing access to chart-level
|
||||
attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and
|
||||
:meth:`Chart.replace_data`. The data structure varies between major chart categories such as
|
||||
category charts and XY charts.
|
||||
"""
|
||||
|
||||
def __init__(self, number_format="General"):
|
||||
super(_BaseChartData, self).__init__()
|
||||
self._number_format = number_format
|
||||
self._series = []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._series.__getitem__(index)
|
||||
|
||||
def __len__(self):
|
||||
return self._series.__len__()
|
||||
|
||||
def append(self, series):
|
||||
return self._series.append(series)
|
||||
|
||||
def data_point_offset(self, series):
|
||||
"""
|
||||
The total integer number of data points appearing in the series of
|
||||
this chart that are prior to *series* in this sequence.
|
||||
"""
|
||||
count = 0
|
||||
for this_series in self:
|
||||
if series is this_series:
|
||||
return count
|
||||
count += len(this_series)
|
||||
raise ValueError("series not in chart data object")
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
The formatting template string, e.g. '#,##0.0', that determines how
|
||||
X and Y values are formatted in this chart and in the Excel
|
||||
spreadsheet. A number format specified on a series will override this
|
||||
value for that series. Likewise, a distinct number format can be
|
||||
specified for a particular data point within a series.
|
||||
"""
|
||||
return self._number_format
|
||||
|
||||
def series_index(self, series):
|
||||
"""
|
||||
Return the integer index of *series* in this sequence.
|
||||
"""
|
||||
for idx, s in enumerate(self):
|
||||
if series is s:
|
||||
return idx
|
||||
raise ValueError("series not in chart data object")
|
||||
|
||||
def series_name_ref(self, series):
|
||||
"""
|
||||
Return the Excel worksheet reference to the cell containing the name
|
||||
for *series*.
|
||||
"""
|
||||
return self._workbook_writer.series_name_ref(series)
|
||||
|
||||
def x_values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the X values for *series* (not
|
||||
including the column label).
|
||||
"""
|
||||
return self._workbook_writer.x_values_ref(series)
|
||||
|
||||
@property
|
||||
def xlsx_blob(self):
|
||||
"""
|
||||
Return a blob containing an Excel workbook file populated with the
|
||||
contents of this chart data object.
|
||||
"""
|
||||
return self._workbook_writer.xlsx_blob
|
||||
|
||||
def xml_bytes(self, chart_type):
|
||||
"""
|
||||
Return a blob containing the XML for a chart of *chart_type*
|
||||
containing the series in this chart data object, as bytes suitable
|
||||
for writing directly to a file.
|
||||
"""
|
||||
return self._xml(chart_type).encode("utf-8")
|
||||
|
||||
def y_values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the Y values for *series* (not
|
||||
including the column label).
|
||||
"""
|
||||
return self._workbook_writer.y_values_ref(series)
|
||||
|
||||
@property
|
||||
def _workbook_writer(self):
|
||||
"""
|
||||
The worksheet writer object to which layout and writing of the Excel
|
||||
worksheet for this chart will be delegated.
|
||||
"""
|
||||
raise NotImplementedError("must be implemented by all subclasses")
|
||||
|
||||
def _xml(self, chart_type):
|
||||
"""
|
||||
Return (as unicode text) the XML for a chart of *chart_type*
|
||||
populated with the values in this chart data object. The XML is
|
||||
a complete XML document, including an XML declaration specifying
|
||||
UTF-8 encoding.
|
||||
"""
|
||||
return ChartXmlWriter(chart_type, self).xml
|
||||
|
||||
|
||||
class _BaseSeriesData(Sequence):
|
||||
"""
|
||||
Base class providing common members for series data objects. A series
|
||||
data object serves as proxy for a series data column in the Excel
|
||||
worksheet. It operates as a sequence of data points, as well as providing
|
||||
access to series-level attributes like the series label.
|
||||
"""
|
||||
|
||||
def __init__(self, chart_data, name, number_format):
|
||||
self._chart_data = chart_data
|
||||
self._name = name
|
||||
self._number_format = number_format
|
||||
self._data_points = []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._data_points.__getitem__(index)
|
||||
|
||||
def __len__(self):
|
||||
return self._data_points.__len__()
|
||||
|
||||
def append(self, data_point):
|
||||
return self._data_points.append(data_point)
|
||||
|
||||
@property
|
||||
def data_point_offset(self):
|
||||
"""
|
||||
The integer count of data points that appear in all chart series
|
||||
prior to this one.
|
||||
"""
|
||||
return self._chart_data.data_point_offset(self)
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""
|
||||
Zero-based integer indicating the sequence position of this series in
|
||||
its chart. For example, the second of three series would return `1`.
|
||||
"""
|
||||
return self._chart_data.series_index(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
The name of this series, e.g. 'Series 1'. This name is used as the
|
||||
column heading for the y-values of this series and may also appear in
|
||||
the chart legend and perhaps other chart locations.
|
||||
"""
|
||||
return self._name if self._name is not None else ""
|
||||
|
||||
@property
|
||||
def name_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the cell containing the name for
|
||||
this series.
|
||||
"""
|
||||
return self._chart_data.series_name_ref(self)
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
The formatting template string that determines how a number in this
|
||||
series is formatted, both in the chart and in the Excel spreadsheet;
|
||||
for example '#,##0.0'. If not specified for this series, it is
|
||||
inherited from the parent chart data object.
|
||||
"""
|
||||
number_format = self._number_format
|
||||
if number_format is None:
|
||||
return self._chart_data.number_format
|
||||
return number_format
|
||||
|
||||
@property
|
||||
def x_values(self):
|
||||
"""
|
||||
A sequence containing the X value of each datapoint in this series,
|
||||
in data point order.
|
||||
"""
|
||||
return [dp.x for dp in self._data_points]
|
||||
|
||||
@property
|
||||
def x_values_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the X values for this chart (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._chart_data.x_values_ref(self)
|
||||
|
||||
@property
|
||||
def y_values(self):
|
||||
"""
|
||||
A sequence containing the Y value of each datapoint in this series,
|
||||
in data point order.
|
||||
"""
|
||||
return [dp.y for dp in self._data_points]
|
||||
|
||||
@property
|
||||
def y_values_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the Y values for this chart (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._chart_data.y_values_ref(self)
|
||||
|
||||
|
||||
class _BaseDataPoint(object):
|
||||
"""
|
||||
Base class providing common members for data point objects.
|
||||
"""
|
||||
|
||||
def __init__(self, series_data, number_format):
|
||||
super(_BaseDataPoint, self).__init__()
|
||||
self._series_data = series_data
|
||||
self._number_format = number_format
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
The formatting template string that determines how the value of this
|
||||
data point is formatted, both in the chart and in the Excel
|
||||
spreadsheet; for example '#,##0.0'. If not specified for this data
|
||||
point, it is inherited from the parent series data object.
|
||||
"""
|
||||
number_format = self._number_format
|
||||
if number_format is None:
|
||||
return self._series_data.number_format
|
||||
return number_format
|
||||
|
||||
|
||||
class CategoryChartData(_BaseChartData):
|
||||
"""
|
||||
Accumulates data specifying the categories and series values for a chart
|
||||
and acts as a proxy for the chart data table that will be written to an
|
||||
Excel worksheet. Used as a parameter in :meth:`shapes.add_chart` and
|
||||
:meth:`Chart.replace_data`.
|
||||
|
||||
This object is suitable for use with category charts, i.e. all those
|
||||
having a discrete set of label values (categories) as the range of their
|
||||
independent variable (X-axis) values. Unlike the ChartData types for
|
||||
charts supporting a continuous range of independent variable values (such
|
||||
as XyChartData), CategoryChartData has a single collection of category
|
||||
(X) values and each data point in its series specifies only the Y value.
|
||||
The corresponding X value is inferred by its position in the sequence.
|
||||
"""
|
||||
|
||||
def add_category(self, label):
|
||||
"""
|
||||
Return a newly created |data.Category| object having *label* and
|
||||
appended to the end of the category collection for this chart.
|
||||
*label* can be a string, a number, a datetime.date, or
|
||||
datetime.datetime object. All category labels in a chart must be the
|
||||
same type. All category labels in a chart having multi-level
|
||||
categories must be strings.
|
||||
"""
|
||||
return self.categories.add_category(label)
|
||||
|
||||
def add_series(self, name, values=(), number_format=None):
|
||||
"""
|
||||
Add a series to this data set entitled *name* and having the data
|
||||
points specified by *values*, an iterable of numeric values.
|
||||
*number_format* specifies how the series values will be displayed,
|
||||
and may be a string, e.g. '#,##0' corresponding to an Excel number
|
||||
format.
|
||||
"""
|
||||
series_data = CategorySeriesData(self, name, number_format)
|
||||
self.append(series_data)
|
||||
for value in values:
|
||||
series_data.add_data_point(value)
|
||||
return series_data
|
||||
|
||||
@property
|
||||
def categories(self):
|
||||
"""|data.Categories| object providing access to category-object hierarchy.
|
||||
|
||||
Assigning an iterable of category labels (strings, numbers, or dates) replaces
|
||||
the |data.Categories| object with a new one containing a category for each label
|
||||
in the sequence.
|
||||
|
||||
Creating a chart from chart data having date categories will cause the chart to
|
||||
have a |DateAxis| for its category axis.
|
||||
"""
|
||||
if not getattr(self, "_categories", False):
|
||||
self._categories = Categories()
|
||||
return self._categories
|
||||
|
||||
@categories.setter
|
||||
def categories(self, category_labels):
|
||||
categories = Categories()
|
||||
for label in category_labels:
|
||||
categories.add_category(label)
|
||||
self._categories = categories
|
||||
|
||||
@property
|
||||
def categories_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the categories for this chart (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._workbook_writer.categories_ref
|
||||
|
||||
def values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the values for *series* (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._workbook_writer.values_ref(series)
|
||||
|
||||
@lazyproperty
|
||||
def _workbook_writer(self):
|
||||
"""
|
||||
The worksheet writer object to which layout and writing of the Excel
|
||||
worksheet for this chart will be delegated.
|
||||
"""
|
||||
return CategoryWorkbookWriter(self)
|
||||
|
||||
|
||||
class Categories(Sequence):
|
||||
"""
|
||||
A sequence of |data.Category| objects, also having certain hierarchical
|
||||
graph behaviors for support of multi-level (nested) categories.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(Categories, self).__init__()
|
||||
self._categories = []
|
||||
self._number_format = None
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self._categories.__getitem__(idx)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the count of the highest level of category in this sequence.
|
||||
If it contains hierarchical (multi-level) categories, this number
|
||||
will differ from :attr:`category_count`, which is the number of leaf
|
||||
nodes.
|
||||
"""
|
||||
return self._categories.__len__()
|
||||
|
||||
def add_category(self, label):
|
||||
"""
|
||||
Return a newly created |data.Category| object having *label* and
|
||||
appended to the end of this category sequence. *label* can be
|
||||
a string, a number, a datetime.date, or datetime.datetime object. All
|
||||
category labels in a chart must be the same type. All category labels
|
||||
in a chart having multi-level categories must be strings.
|
||||
|
||||
Creating a chart from chart data having date categories will cause
|
||||
the chart to have a |DateAxis| for its category axis.
|
||||
"""
|
||||
category = Category(label, self)
|
||||
self._categories.append(category)
|
||||
return category
|
||||
|
||||
@property
|
||||
def are_dates(self):
|
||||
"""
|
||||
Return |True| if the first category in this collection has a date
|
||||
label (as opposed to str or numeric). A date label is one of type
|
||||
datetime.date or datetime.datetime. Returns |False| otherwise,
|
||||
including when this category collection is empty. It also returns
|
||||
False when this category collection is hierarchical, because
|
||||
hierarchical categories can only be written as string labels.
|
||||
"""
|
||||
if self.depth != 1:
|
||||
return False
|
||||
first_cat_label = self[0].label
|
||||
date_types = (datetime.date, datetime.datetime)
|
||||
if isinstance(first_cat_label, date_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def are_numeric(self):
|
||||
"""
|
||||
Return |True| if the first category in this collection has a numeric
|
||||
label (as opposed to a string label), including if that value is
|
||||
a datetime.date or datetime.datetime object (as those are converted
|
||||
to integers for storage in Excel). Returns |False| otherwise,
|
||||
including when this category collection is empty. It also returns
|
||||
False when this category collection is hierarchical, because
|
||||
hierarchical categories can only be written as string labels.
|
||||
"""
|
||||
if self.depth != 1:
|
||||
return False
|
||||
# This method only tests the first category. The categories must
|
||||
# be of uniform type, and if they're not, there will be problems
|
||||
# later in the process, but it's not this method's job to validate
|
||||
# the caller's input.
|
||||
first_cat_label = self[0].label
|
||||
numeric_types = (Number, datetime.date, datetime.datetime)
|
||||
if isinstance(first_cat_label, numeric_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
"""
|
||||
The number of hierarchy levels in this category graph. Returns 0 if
|
||||
it contains no categories.
|
||||
"""
|
||||
categories = self._categories
|
||||
if not categories:
|
||||
return 0
|
||||
first_depth = categories[0].depth
|
||||
for category in categories[1:]:
|
||||
if category.depth != first_depth:
|
||||
raise ValueError("category depth not uniform")
|
||||
return first_depth
|
||||
|
||||
def index(self, category):
|
||||
"""
|
||||
The offset of *category* in the overall sequence of leaf categories.
|
||||
A non-leaf category gets the index of its first sub-category.
|
||||
"""
|
||||
index = 0
|
||||
for this_category in self._categories:
|
||||
if category is this_category:
|
||||
return index
|
||||
index += this_category.leaf_count
|
||||
raise ValueError("category not in top-level categories")
|
||||
|
||||
@property
|
||||
def leaf_count(self):
|
||||
"""
|
||||
The number of leaf-level categories in this hierarchy. The return
|
||||
value is the same as that of `len()` only when the hierarchy is
|
||||
single level.
|
||||
"""
|
||||
return sum(c.leaf_count for c in self._categories)
|
||||
|
||||
@property
|
||||
def levels(self):
|
||||
"""
|
||||
A generator of (idx, label) sequences representing the category
|
||||
hierarchy from the bottom up. The first level contains all leaf
|
||||
categories, and each subsequent is the next level up.
|
||||
"""
|
||||
|
||||
def levels(categories):
|
||||
# yield all lower levels
|
||||
sub_categories = [sc for c in categories for sc in c.sub_categories]
|
||||
if sub_categories:
|
||||
for level in levels(sub_categories):
|
||||
yield level
|
||||
# yield this level
|
||||
yield [(cat.idx, cat.label) for cat in categories]
|
||||
|
||||
for level in levels(self):
|
||||
yield level
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
Read/write. Return a string representing the number format used in
|
||||
Excel to format these category values, e.g. '0.0' or 'mm/dd/yyyy'.
|
||||
This string is only relevant when the categories are numeric or date
|
||||
type, although it returns 'General' without error when the categories
|
||||
are string labels. Assigning |None| causes the default number format
|
||||
to be used, based on the type of the category labels.
|
||||
"""
|
||||
GENERAL = "General"
|
||||
|
||||
# defined value takes precedence
|
||||
if self._number_format is not None:
|
||||
return self._number_format
|
||||
|
||||
# multi-level (should) always be string labels
|
||||
# zero depth means empty in which case we can't tell anyway
|
||||
if self.depth != 1:
|
||||
return GENERAL
|
||||
|
||||
# everything except dates gets 'General'
|
||||
first_cat_label = self[0].label
|
||||
if isinstance(first_cat_label, (datetime.date, datetime.datetime)):
|
||||
return r"yyyy\-mm\-dd"
|
||||
return GENERAL
|
||||
|
||||
@number_format.setter
|
||||
def number_format(self, value):
|
||||
self._number_format = value
|
||||
|
||||
|
||||
class Category(object):
|
||||
"""
|
||||
A chart category, primarily having a label to be displayed on the
|
||||
category axis, but also able to be configured in a hierarchy for support
|
||||
of multi-level category charts.
|
||||
"""
|
||||
|
||||
def __init__(self, label, parent):
|
||||
super(Category, self).__init__()
|
||||
self._label = label
|
||||
self._parent = parent
|
||||
self._sub_categories = []
|
||||
|
||||
def add_sub_category(self, label):
|
||||
"""
|
||||
Return a newly created |data.Category| object having *label* and
|
||||
appended to the end of the sub-category sequence for this category.
|
||||
"""
|
||||
category = Category(label, self)
|
||||
self._sub_categories.append(category)
|
||||
return category
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
"""
|
||||
The number of hierarchy levels rooted at this category node. Returns
|
||||
1 if this category has no sub-categories.
|
||||
"""
|
||||
sub_categories = self._sub_categories
|
||||
if not sub_categories:
|
||||
return 1
|
||||
first_depth = sub_categories[0].depth
|
||||
for category in sub_categories[1:]:
|
||||
if category.depth != first_depth:
|
||||
raise ValueError("category depth not uniform")
|
||||
return first_depth + 1
|
||||
|
||||
@property
|
||||
def idx(self):
|
||||
"""
|
||||
The offset of this category in the overall sequence of leaf
|
||||
categories. A non-leaf category gets the index of its first
|
||||
sub-category.
|
||||
"""
|
||||
return self._parent.index(self)
|
||||
|
||||
def index(self, sub_category):
|
||||
"""
|
||||
The offset of *sub_category* in the overall sequence of leaf
|
||||
categories.
|
||||
"""
|
||||
index = self._parent.index(self)
|
||||
for this_sub_category in self._sub_categories:
|
||||
if sub_category is this_sub_category:
|
||||
return index
|
||||
index += this_sub_category.leaf_count
|
||||
raise ValueError("sub_category not in this category")
|
||||
|
||||
@property
|
||||
def leaf_count(self):
|
||||
"""
|
||||
The number of leaf category nodes under this category. Returns
|
||||
1 if this category has no sub-categories.
|
||||
"""
|
||||
if not self._sub_categories:
|
||||
return 1
|
||||
return sum(category.leaf_count for category in self._sub_categories)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
"""
|
||||
The value that appears on the axis for this category. The label can
|
||||
be a string, a number, or a datetime.date or datetime.datetime
|
||||
object.
|
||||
"""
|
||||
return self._label if self._label is not None else ""
|
||||
|
||||
def numeric_str_val(self, date_1904=False):
|
||||
"""
|
||||
The string representation of the numeric (or date) label of this
|
||||
category, suitable for use in the XML `c:pt` element for this
|
||||
category. The optional *date_1904* parameter specifies the epoch used
|
||||
for calculating Excel date numbers.
|
||||
"""
|
||||
label = self._label
|
||||
if isinstance(label, (datetime.date, datetime.datetime)):
|
||||
return "%.1f" % self._excel_date_number(date_1904)
|
||||
return str(self._label)
|
||||
|
||||
@property
|
||||
def sub_categories(self):
|
||||
"""
|
||||
The sequence of child categories for this category.
|
||||
"""
|
||||
return self._sub_categories
|
||||
|
||||
def _excel_date_number(self, date_1904):
|
||||
"""
|
||||
Return an integer representing the date label of this category as the
|
||||
number of days since January 1, 1900 (or 1904 if date_1904 is
|
||||
|True|).
|
||||
"""
|
||||
date, label = datetime.date, self._label
|
||||
# -- get date from label in type-independent-ish way
|
||||
date_ = date(label.year, label.month, label.day)
|
||||
epoch = date(1904, 1, 1) if date_1904 else date(1899, 12, 31)
|
||||
delta = date_ - epoch
|
||||
excel_day_number = delta.days
|
||||
|
||||
# -- adjust for Excel mistaking 1900 for a leap year --
|
||||
if not date_1904 and excel_day_number > 59:
|
||||
excel_day_number += 1
|
||||
|
||||
return excel_day_number
|
||||
|
||||
|
||||
class ChartData(CategoryChartData):
|
||||
"""
|
||||
|ChartData| is simply an alias for |CategoryChartData| and may be removed
|
||||
in a future release. All new development should use |CategoryChartData|
|
||||
for creating or replacing the data in chart types other than XY and
|
||||
Bubble.
|
||||
"""
|
||||
|
||||
|
||||
class CategorySeriesData(_BaseSeriesData):
|
||||
"""
|
||||
The data specific to a particular category chart series. It provides
|
||||
access to the series label, the series data points, and an optional
|
||||
number format to be applied to each data point not having a specified
|
||||
number format.
|
||||
"""
|
||||
|
||||
def add_data_point(self, value, number_format=None):
|
||||
"""
|
||||
Return a CategoryDataPoint object newly created with value *value*,
|
||||
an optional *number_format*, and appended to this sequence.
|
||||
"""
|
||||
data_point = CategoryDataPoint(self, value, number_format)
|
||||
self.append(data_point)
|
||||
return data_point
|
||||
|
||||
@property
|
||||
def categories(self):
|
||||
"""
|
||||
The |data.Categories| object that provides access to the category
|
||||
objects for this series.
|
||||
"""
|
||||
return self._chart_data.categories
|
||||
|
||||
@property
|
||||
def categories_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the categories for this chart (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._chart_data.categories_ref
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
"""
|
||||
A sequence containing the (Y) value of each datapoint in this series,
|
||||
in data point order.
|
||||
"""
|
||||
return [dp.value for dp in self._data_points]
|
||||
|
||||
@property
|
||||
def values_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the (Y) values for this series (not
|
||||
including the column heading).
|
||||
"""
|
||||
return self._chart_data.values_ref(self)
|
||||
|
||||
|
||||
class XyChartData(_BaseChartData):
|
||||
"""
|
||||
A specialized ChartData object suitable for use with an XY (aka. scatter)
|
||||
chart. Unlike ChartData, it has no category sequence. Rather, each data
|
||||
point of each series specifies both an X and a Y value.
|
||||
"""
|
||||
|
||||
def add_series(self, name, number_format=None):
|
||||
"""
|
||||
Return an |XySeriesData| object newly created and added at the end of
|
||||
this sequence, identified by *name* and values formatted with
|
||||
*number_format*.
|
||||
"""
|
||||
series_data = XySeriesData(self, name, number_format)
|
||||
self.append(series_data)
|
||||
return series_data
|
||||
|
||||
@lazyproperty
|
||||
def _workbook_writer(self):
|
||||
"""
|
||||
The worksheet writer object to which layout and writing of the Excel
|
||||
worksheet for this chart will be delegated.
|
||||
"""
|
||||
return XyWorkbookWriter(self)
|
||||
|
||||
|
||||
class BubbleChartData(XyChartData):
|
||||
"""
|
||||
A specialized ChartData object suitable for use with a bubble chart.
|
||||
A bubble chart is essentially an XY chart where the markers are scaled to
|
||||
provide a third quantitative dimension to the exhibit.
|
||||
"""
|
||||
|
||||
def add_series(self, name, number_format=None):
|
||||
"""
|
||||
Return a |BubbleSeriesData| object newly created and added at the end
|
||||
of this sequence, and having series named *name* and values formatted
|
||||
with *number_format*.
|
||||
"""
|
||||
series_data = BubbleSeriesData(self, name, number_format)
|
||||
self.append(series_data)
|
||||
return series_data
|
||||
|
||||
def bubble_sizes_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference for the range containing the bubble
|
||||
sizes for *series*.
|
||||
"""
|
||||
return self._workbook_writer.bubble_sizes_ref(series)
|
||||
|
||||
@lazyproperty
|
||||
def _workbook_writer(self):
|
||||
"""
|
||||
The worksheet writer object to which layout and writing of the Excel
|
||||
worksheet for this chart will be delegated.
|
||||
"""
|
||||
return BubbleWorkbookWriter(self)
|
||||
|
||||
|
||||
class XySeriesData(_BaseSeriesData):
|
||||
"""
|
||||
The data specific to a particular XY chart series. It provides access to
|
||||
the series label, the series data points, and an optional number format
|
||||
to be applied to each data point not having a specified number format.
|
||||
|
||||
The sequence of data points in an XY series is significant; lines are
|
||||
plotted following the sequence of points, even if that causes a line
|
||||
segment to "travel backward" (implying a multi-valued function). The data
|
||||
points are not automatically sorted into increasing order by X value.
|
||||
"""
|
||||
|
||||
def add_data_point(self, x, y, number_format=None):
|
||||
"""
|
||||
Return an XyDataPoint object newly created with values *x* and *y*,
|
||||
and appended to this sequence.
|
||||
"""
|
||||
data_point = XyDataPoint(self, x, y, number_format)
|
||||
self.append(data_point)
|
||||
return data_point
|
||||
|
||||
|
||||
class BubbleSeriesData(XySeriesData):
|
||||
"""
|
||||
The data specific to a particular Bubble chart series. It provides access
|
||||
to the series label, the series data points, and an optional number
|
||||
format to be applied to each data point not having a specified number
|
||||
format.
|
||||
|
||||
The sequence of data points in a bubble chart series is maintained
|
||||
throughout the chart building process because a data point has no unique
|
||||
identifier and can only be retrieved by index.
|
||||
"""
|
||||
|
||||
def add_data_point(self, x, y, size, number_format=None):
|
||||
"""
|
||||
Append a new BubbleDataPoint object having the values *x*, *y*, and
|
||||
*size*. The optional *number_format* is used to format the Y value.
|
||||
If not provided, the number format is inherited from the series data.
|
||||
"""
|
||||
data_point = BubbleDataPoint(self, x, y, size, number_format)
|
||||
self.append(data_point)
|
||||
return data_point
|
||||
|
||||
@property
|
||||
def bubble_sizes(self):
|
||||
"""
|
||||
A sequence containing the bubble size for each datapoint in this
|
||||
series, in data point order.
|
||||
"""
|
||||
return [dp.bubble_size for dp in self._data_points]
|
||||
|
||||
@property
|
||||
def bubble_sizes_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference for the range containing the bubble
|
||||
sizes for this series.
|
||||
"""
|
||||
return self._chart_data.bubble_sizes_ref(self)
|
||||
|
||||
|
||||
class CategoryDataPoint(_BaseDataPoint):
|
||||
"""
|
||||
A data point in a category chart series. Provides access to the value of
|
||||
the datapoint and the number format with which it should appear in the
|
||||
Excel file.
|
||||
"""
|
||||
|
||||
def __init__(self, series_data, value, number_format):
|
||||
super(CategoryDataPoint, self).__init__(series_data, number_format)
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
The (Y) value for this category data point.
|
||||
"""
|
||||
return self._value
|
||||
|
||||
|
||||
class XyDataPoint(_BaseDataPoint):
|
||||
"""
|
||||
A data point in an XY chart series. Provides access to the x and y values
|
||||
of the datapoint.
|
||||
"""
|
||||
|
||||
def __init__(self, series_data, x, y, number_format):
|
||||
super(XyDataPoint, self).__init__(series_data, number_format)
|
||||
self._x = x
|
||||
self._y = y
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"""
|
||||
The X value for this XY data point.
|
||||
"""
|
||||
return self._x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""
|
||||
The Y value for this XY data point.
|
||||
"""
|
||||
return self._y
|
||||
|
||||
|
||||
class BubbleDataPoint(XyDataPoint):
|
||||
"""
|
||||
A data point in a bubble chart series. Provides access to the x, y, and
|
||||
size values of the datapoint.
|
||||
"""
|
||||
|
||||
def __init__(self, series_data, x, y, size, number_format):
|
||||
super(BubbleDataPoint, self).__init__(series_data, x, y, number_format)
|
||||
self._size = size
|
||||
|
||||
@property
|
||||
def bubble_size(self):
|
||||
"""
|
||||
The value representing the size of the bubble for this data point.
|
||||
"""
|
||||
return self._size
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Data label-related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.text.text import Font, TextFrame
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class DataLabels(object):
|
||||
"""Provides access to properties of data labels for a plot or a series.
|
||||
|
||||
This is not a collection and does not provide access to individual data
|
||||
labels. Access to individual labels is via the |Point| object. The
|
||||
properties this object provides control formatting of *all* the data
|
||||
labels in its scope.
|
||||
"""
|
||||
|
||||
def __init__(self, dLbls):
|
||||
super(DataLabels, self).__init__()
|
||||
self._element = dLbls
|
||||
|
||||
@lazyproperty
|
||||
def font(self):
|
||||
"""
|
||||
The |Font| object that provides access to the text properties for
|
||||
these data labels, such as bold, italic, etc.
|
||||
"""
|
||||
defRPr = self._element.defRPr
|
||||
font = Font(defRPr)
|
||||
return font
|
||||
|
||||
@property
|
||||
def number_format(self):
|
||||
"""
|
||||
Read/write string specifying the format for the numbers on this set
|
||||
of data labels. Returns 'General' if no number format has been set.
|
||||
Note that this format string has no effect on rendered data labels
|
||||
when :meth:`number_format_is_linked` is |True|. Assigning a format
|
||||
string to this property automatically sets
|
||||
:meth:`number_format_is_linked` to |False|.
|
||||
"""
|
||||
numFmt = self._element.numFmt
|
||||
if numFmt is None:
|
||||
return "General"
|
||||
return numFmt.formatCode
|
||||
|
||||
@number_format.setter
|
||||
def number_format(self, value):
|
||||
self._element.get_or_add_numFmt().formatCode = value
|
||||
self.number_format_is_linked = False
|
||||
|
||||
@property
|
||||
def number_format_is_linked(self):
|
||||
"""
|
||||
Read/write boolean specifying whether number formatting should be
|
||||
taken from the source spreadsheet rather than the value of
|
||||
:meth:`number_format`.
|
||||
"""
|
||||
numFmt = self._element.numFmt
|
||||
if numFmt is None:
|
||||
return True
|
||||
souceLinked = numFmt.sourceLinked
|
||||
if souceLinked is None:
|
||||
return True
|
||||
return numFmt.sourceLinked
|
||||
|
||||
@number_format_is_linked.setter
|
||||
def number_format_is_linked(self, value):
|
||||
numFmt = self._element.get_or_add_numFmt()
|
||||
numFmt.sourceLinked = value
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
Read/write :ref:`XlDataLabelPosition` enumeration value specifying
|
||||
the position of the data labels with respect to their data point, or
|
||||
|None| if no position is specified. Assigning |None| causes
|
||||
PowerPoint to choose the default position, which varies by chart
|
||||
type.
|
||||
"""
|
||||
dLblPos = self._element.dLblPos
|
||||
if dLblPos is None:
|
||||
return None
|
||||
return dLblPos.val
|
||||
|
||||
@position.setter
|
||||
def position(self, value):
|
||||
if value is None:
|
||||
self._element._remove_dLblPos()
|
||||
return
|
||||
self._element.get_or_add_dLblPos().val = value
|
||||
|
||||
@property
|
||||
def show_category_name(self):
|
||||
"""Read/write. True when name of category should appear in label."""
|
||||
return self._element.get_or_add_showCatName().val
|
||||
|
||||
@show_category_name.setter
|
||||
def show_category_name(self, value):
|
||||
self._element.get_or_add_showCatName().val = bool(value)
|
||||
|
||||
@property
|
||||
def show_legend_key(self):
|
||||
"""Read/write. True when data label displays legend-color swatch."""
|
||||
return self._element.get_or_add_showLegendKey().val
|
||||
|
||||
@show_legend_key.setter
|
||||
def show_legend_key(self, value):
|
||||
self._element.get_or_add_showLegendKey().val = bool(value)
|
||||
|
||||
@property
|
||||
def show_percentage(self):
|
||||
"""Read/write. True when data label displays percentage.
|
||||
|
||||
This option is not operative on all chart types. Percentage appears
|
||||
on polar charts such as pie and donut.
|
||||
"""
|
||||
return self._element.get_or_add_showPercent().val
|
||||
|
||||
@show_percentage.setter
|
||||
def show_percentage(self, value):
|
||||
self._element.get_or_add_showPercent().val = bool(value)
|
||||
|
||||
@property
|
||||
def show_series_name(self):
|
||||
"""Read/write. True when data label displays series name."""
|
||||
return self._element.get_or_add_showSerName().val
|
||||
|
||||
@show_series_name.setter
|
||||
def show_series_name(self, value):
|
||||
self._element.get_or_add_showSerName().val = bool(value)
|
||||
|
||||
@property
|
||||
def show_value(self):
|
||||
"""Read/write. True when label displays numeric value of datapoint."""
|
||||
return self._element.get_or_add_showVal().val
|
||||
|
||||
@show_value.setter
|
||||
def show_value(self, value):
|
||||
self._element.get_or_add_showVal().val = bool(value)
|
||||
|
||||
|
||||
class DataLabel(object):
|
||||
"""
|
||||
The data label associated with an individual data point.
|
||||
"""
|
||||
|
||||
def __init__(self, ser, idx):
|
||||
super(DataLabel, self).__init__()
|
||||
self._ser = self._element = ser
|
||||
self._idx = idx
|
||||
|
||||
@lazyproperty
|
||||
def font(self):
|
||||
"""The |Font| object providing text formatting for this data label.
|
||||
|
||||
This font object is used to customize the appearance of automatically
|
||||
inserted text, such as the data point value. The font applies to the
|
||||
entire data label. More granular control of the appearance of custom
|
||||
data label text is controlled by a font object on runs in the text
|
||||
frame.
|
||||
"""
|
||||
txPr = self._get_or_add_txPr()
|
||||
text_frame = TextFrame(txPr, self)
|
||||
paragraph = text_frame.paragraphs[0]
|
||||
return paragraph.font
|
||||
|
||||
@property
|
||||
def has_text_frame(self):
|
||||
"""
|
||||
Return |True| if this data label has a text frame (implying it has
|
||||
custom data label text), and |False| otherwise. Assigning |True|
|
||||
causes a text frame to be added if not already present. Assigning
|
||||
|False| causes any existing text frame to be removed along with any
|
||||
text contained in the text frame.
|
||||
"""
|
||||
dLbl = self._dLbl
|
||||
if dLbl is None:
|
||||
return False
|
||||
if dLbl.xpath("c:tx/c:rich"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@has_text_frame.setter
|
||||
def has_text_frame(self, value):
|
||||
if bool(value) is True:
|
||||
self._get_or_add_tx_rich()
|
||||
else:
|
||||
self._remove_tx_rich()
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
Read/write :ref:`XlDataLabelPosition` member specifying the position
|
||||
of this data label with respect to its data point, or |None| if no
|
||||
position is specified. Assigning |None| causes PowerPoint to choose
|
||||
the default position, which varies by chart type.
|
||||
"""
|
||||
dLbl = self._dLbl
|
||||
if dLbl is None:
|
||||
return None
|
||||
dLblPos = dLbl.dLblPos
|
||||
if dLblPos is None:
|
||||
return None
|
||||
return dLblPos.val
|
||||
|
||||
@position.setter
|
||||
def position(self, value):
|
||||
if value is None:
|
||||
dLbl = self._dLbl
|
||||
if dLbl is None:
|
||||
return
|
||||
dLbl._remove_dLblPos()
|
||||
return
|
||||
dLbl = self._get_or_add_dLbl()
|
||||
dLbl.get_or_add_dLblPos().val = value
|
||||
|
||||
@property
|
||||
def text_frame(self):
|
||||
"""
|
||||
|TextFrame| instance for this data label, containing the text of the
|
||||
data label and providing access to its text formatting properties.
|
||||
"""
|
||||
rich = self._get_or_add_rich()
|
||||
return TextFrame(rich, self)
|
||||
|
||||
@property
|
||||
def _dLbl(self):
|
||||
"""
|
||||
Return the |CT_DLbl| instance referring specifically to this
|
||||
individual data label (having the same index value), or |None| if not
|
||||
present.
|
||||
"""
|
||||
return self._ser.get_dLbl(self._idx)
|
||||
|
||||
def _get_or_add_dLbl(self):
|
||||
"""
|
||||
The ``CT_DLbl`` instance referring specifically to this individual
|
||||
data label, newly created if not yet present in the XML.
|
||||
"""
|
||||
return self._ser.get_or_add_dLbl(self._idx)
|
||||
|
||||
def _get_or_add_rich(self):
|
||||
"""
|
||||
Return the `c:rich` element representing the text frame for this data
|
||||
label, newly created with its ancestors if not present.
|
||||
"""
|
||||
dLbl = self._get_or_add_dLbl()
|
||||
|
||||
# having a c:spPr or c:txPr when a c:tx is present causes the "can't
|
||||
# save" bug on bubble charts. Remove c:spPr and c:txPr when present.
|
||||
dLbl._remove_spPr()
|
||||
dLbl._remove_txPr()
|
||||
|
||||
return dLbl.get_or_add_rich()
|
||||
|
||||
def _get_or_add_tx_rich(self):
|
||||
"""
|
||||
Return the `c:tx` element for this data label, with its `c:rich`
|
||||
child and descendants, newly created if not yet present.
|
||||
"""
|
||||
dLbl = self._get_or_add_dLbl()
|
||||
|
||||
# having a c:spPr or c:txPr when a c:tx is present causes the "can't
|
||||
# save" bug on bubble charts. Remove c:spPr and c:txPr when present.
|
||||
dLbl._remove_spPr()
|
||||
dLbl._remove_txPr()
|
||||
|
||||
return dLbl.get_or_add_tx_rich()
|
||||
|
||||
def _get_or_add_txPr(self):
|
||||
"""Return the `c:txPr` element for this data label.
|
||||
|
||||
The `c:txPr` element and its parent `c:dLbl` element are created if
|
||||
not yet present.
|
||||
"""
|
||||
dLbl = self._get_or_add_dLbl()
|
||||
return dLbl.get_or_add_txPr()
|
||||
|
||||
def _remove_tx_rich(self):
|
||||
"""
|
||||
Remove any `c:tx/c:rich` child of the `c:dLbl` element for this data
|
||||
label. Do nothing if that element is not present.
|
||||
"""
|
||||
dLbl = self._dLbl
|
||||
if dLbl is None:
|
||||
return
|
||||
dLbl.remove_tx_rich()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Legend of a chart."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.chart import XL_LEGEND_POSITION
|
||||
from pptx.text.text import Font
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class Legend(object):
|
||||
"""
|
||||
Represents the legend in a chart. A chart can have at most one legend.
|
||||
"""
|
||||
|
||||
def __init__(self, legend_elm):
|
||||
super(Legend, self).__init__()
|
||||
self._element = legend_elm
|
||||
|
||||
@lazyproperty
|
||||
def font(self):
|
||||
"""
|
||||
The |Font| object that provides access to the text properties for
|
||||
this legend, such as bold, italic, etc.
|
||||
"""
|
||||
defRPr = self._element.defRPr
|
||||
font = Font(defRPr)
|
||||
return font
|
||||
|
||||
@property
|
||||
def horz_offset(self):
|
||||
"""
|
||||
Adjustment of the x position of the legend from its default.
|
||||
Expressed as a float between -1.0 and 1.0 representing a fraction of
|
||||
the chart width. Negative values move the legend left, positive
|
||||
values move it to the right. |None| if no setting is specified.
|
||||
"""
|
||||
return self._element.horz_offset
|
||||
|
||||
@horz_offset.setter
|
||||
def horz_offset(self, value):
|
||||
self._element.horz_offset = value
|
||||
|
||||
@property
|
||||
def include_in_layout(self):
|
||||
"""|True| if legend should be located inside plot area.
|
||||
|
||||
Read/write boolean specifying whether legend should be placed inside
|
||||
the plot area. In many cases this will cause it to be superimposed on
|
||||
the chart itself. Assigning |None| to this property causes any
|
||||
`c:overlay` element to be removed, which is interpreted the same as
|
||||
|True|. This use case should rarely be required and assigning
|
||||
a boolean value is recommended.
|
||||
"""
|
||||
overlay = self._element.overlay
|
||||
if overlay is None:
|
||||
return True
|
||||
return overlay.val
|
||||
|
||||
@include_in_layout.setter
|
||||
def include_in_layout(self, value):
|
||||
if value is None:
|
||||
self._element._remove_overlay()
|
||||
return
|
||||
self._element.get_or_add_overlay().val = bool(value)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
Read/write :ref:`XlLegendPosition` enumeration value specifying the
|
||||
general region of the chart in which to place the legend.
|
||||
"""
|
||||
legendPos = self._element.legendPos
|
||||
if legendPos is None:
|
||||
return XL_LEGEND_POSITION.RIGHT
|
||||
return legendPos.val
|
||||
|
||||
@position.setter
|
||||
def position(self, position):
|
||||
self._element.get_or_add_legendPos().val = position
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Marker-related objects.
|
||||
|
||||
Only the line-type charts Line, XY, and Radar have markers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.dml.chtfmt import ChartFormat
|
||||
from pptx.shared import ElementProxy
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class Marker(ElementProxy):
|
||||
"""
|
||||
Represents a data point marker, such as a diamond or circle, on
|
||||
a line-type chart.
|
||||
"""
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""
|
||||
The |ChartFormat| instance for this marker, providing access to shape
|
||||
properties such as fill and line.
|
||||
"""
|
||||
marker = self._element.get_or_add_marker()
|
||||
return ChartFormat(marker)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""
|
||||
An integer between 2 and 72 inclusive indicating the size of this
|
||||
marker in points. A value of |None| indicates no explicit value is
|
||||
set and the size is inherited from a higher-level setting or the
|
||||
PowerPoint default (which may be 9). Assigning |None| removes any
|
||||
explicitly assigned size, causing this value to be inherited.
|
||||
"""
|
||||
marker = self._element.marker
|
||||
if marker is None:
|
||||
return None
|
||||
return marker.size_val
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
marker = self._element.get_or_add_marker()
|
||||
marker._remove_size()
|
||||
if value is None:
|
||||
return
|
||||
size = marker._add_size()
|
||||
size.val = value
|
||||
|
||||
@property
|
||||
def style(self):
|
||||
"""
|
||||
A member of the :ref:`XlMarkerStyle` enumeration indicating the shape
|
||||
of this marker. Returns |None| if no explicit style has been set,
|
||||
which corresponds to the "Automatic" option in the PowerPoint UI.
|
||||
"""
|
||||
marker = self._element.marker
|
||||
if marker is None:
|
||||
return None
|
||||
return marker.symbol_val
|
||||
|
||||
@style.setter
|
||||
def style(self, value):
|
||||
marker = self._element.get_or_add_marker()
|
||||
marker._remove_symbol()
|
||||
if value is None:
|
||||
return
|
||||
symbol = marker._add_symbol()
|
||||
symbol.val = value
|
||||
@@ -0,0 +1,412 @@
|
||||
"""Plot-related objects.
|
||||
|
||||
A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on
|
||||
each other, such as a line plot layered over a bar plot.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.chart.category import Categories
|
||||
from pptx.chart.datalabel import DataLabels
|
||||
from pptx.chart.series import SeriesCollection
|
||||
from pptx.enum.chart import XL_CHART_TYPE as XL
|
||||
from pptx.oxml.ns import qn
|
||||
from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class _BasePlot(object):
|
||||
"""
|
||||
A distinct plot that appears in the plot area of a chart. A chart may
|
||||
have more than one plot, in which case they appear as superimposed
|
||||
layers, such as a line plot appearing on top of a bar chart.
|
||||
"""
|
||||
|
||||
def __init__(self, xChart, chart):
|
||||
super(_BasePlot, self).__init__()
|
||||
self._element = xChart
|
||||
self._chart = chart
|
||||
|
||||
@lazyproperty
|
||||
def categories(self):
|
||||
"""
|
||||
Returns a |category.Categories| sequence object containing
|
||||
a |category.Category| object for each of the category labels
|
||||
associated with this plot. The |category.Category| class derives from
|
||||
``str``, so the returned value can be treated as a simple sequence of
|
||||
strings for the common case where all you need is the labels in the
|
||||
order they appear on the chart. |category.Categories| provides
|
||||
additional properties for dealing with hierarchical categories when
|
||||
required.
|
||||
"""
|
||||
return Categories(self._element)
|
||||
|
||||
@property
|
||||
def chart(self):
|
||||
"""
|
||||
The |Chart| object containing this plot.
|
||||
"""
|
||||
return self._chart
|
||||
|
||||
@property
|
||||
def data_labels(self):
|
||||
"""
|
||||
|DataLabels| instance providing properties and methods on the
|
||||
collection of data labels associated with this plot.
|
||||
"""
|
||||
dLbls = self._element.dLbls
|
||||
if dLbls is None:
|
||||
raise ValueError("plot has no data labels, set has_data_labels = True first")
|
||||
return DataLabels(dLbls)
|
||||
|
||||
@property
|
||||
def has_data_labels(self):
|
||||
"""
|
||||
Read/write boolean, |True| if the series has data labels. Assigning
|
||||
|True| causes data labels to be added to the plot. Assigning False
|
||||
removes any existing data labels.
|
||||
"""
|
||||
return self._element.dLbls is not None
|
||||
|
||||
@has_data_labels.setter
|
||||
def has_data_labels(self, value):
|
||||
"""
|
||||
Add, remove, or leave alone the ``<c:dLbls>`` child element depending
|
||||
on current state and assigned *value*. If *value* is |True| and no
|
||||
``<c:dLbls>`` element is present, a new default element is added with
|
||||
default child elements and settings. When |False|, any existing dLbls
|
||||
element is removed.
|
||||
"""
|
||||
if bool(value) is False:
|
||||
self._element._remove_dLbls()
|
||||
else:
|
||||
if self._element.dLbls is None:
|
||||
dLbls = self._element._add_dLbls()
|
||||
dLbls.showVal.val = True
|
||||
|
||||
@lazyproperty
|
||||
def series(self):
|
||||
"""
|
||||
A sequence of |Series| objects representing the series in this plot,
|
||||
in the order they appear in the plot.
|
||||
"""
|
||||
return SeriesCollection(self._element)
|
||||
|
||||
@property
|
||||
def vary_by_categories(self):
|
||||
"""
|
||||
Read/write boolean value specifying whether to use a different color
|
||||
for each of the points in this plot. Only effective when there is
|
||||
a single series; PowerPoint automatically varies color by series when
|
||||
more than one series is present.
|
||||
"""
|
||||
varyColors = self._element.varyColors
|
||||
if varyColors is None:
|
||||
return True
|
||||
return varyColors.val
|
||||
|
||||
@vary_by_categories.setter
|
||||
def vary_by_categories(self, value):
|
||||
self._element.get_or_add_varyColors().val = bool(value)
|
||||
|
||||
|
||||
class AreaPlot(_BasePlot):
|
||||
"""
|
||||
An area plot.
|
||||
"""
|
||||
|
||||
|
||||
class Area3DPlot(_BasePlot):
|
||||
"""
|
||||
A 3-dimensional area plot.
|
||||
"""
|
||||
|
||||
|
||||
class BarPlot(_BasePlot):
|
||||
"""
|
||||
A bar chart-style plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
def gap_width(self):
|
||||
"""
|
||||
Width of gap between bar(s) of each category, as an integer
|
||||
percentage of the bar width. The default value for a new bar chart is
|
||||
150, representing 150% or 1.5 times the width of a single bar.
|
||||
"""
|
||||
gapWidth = self._element.gapWidth
|
||||
if gapWidth is None:
|
||||
return 150
|
||||
return gapWidth.val
|
||||
|
||||
@gap_width.setter
|
||||
def gap_width(self, value):
|
||||
gapWidth = self._element.get_or_add_gapWidth()
|
||||
gapWidth.val = value
|
||||
|
||||
@property
|
||||
def overlap(self):
|
||||
"""
|
||||
Read/write int value in range -100..100 specifying a percentage of
|
||||
the bar width by which to overlap adjacent bars in a multi-series bar
|
||||
chart. Default is 0. A setting of -100 creates a gap of a full bar
|
||||
width and a setting of 100 causes all the bars in a category to be
|
||||
superimposed. A stacked bar plot has overlap of 100 by default.
|
||||
"""
|
||||
overlap = self._element.overlap
|
||||
if overlap is None:
|
||||
return 0
|
||||
return overlap.val
|
||||
|
||||
@overlap.setter
|
||||
def overlap(self, value):
|
||||
"""
|
||||
Set the value of the ``<c:overlap>`` child element to *int_value*,
|
||||
or remove the overlap element if *int_value* is 0.
|
||||
"""
|
||||
if value == 0:
|
||||
self._element._remove_overlap()
|
||||
return
|
||||
self._element.get_or_add_overlap().val = value
|
||||
|
||||
|
||||
class BubblePlot(_BasePlot):
|
||||
"""
|
||||
A bubble chart plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
def bubble_scale(self):
|
||||
"""
|
||||
An integer between 0 and 300 inclusive indicating the percentage of
|
||||
the default size at which bubbles should be displayed. Assigning
|
||||
|None| produces the same behavior as assigning `100`.
|
||||
"""
|
||||
bubbleScale = self._element.bubbleScale
|
||||
if bubbleScale is None:
|
||||
return 100
|
||||
return bubbleScale.val
|
||||
|
||||
@bubble_scale.setter
|
||||
def bubble_scale(self, value):
|
||||
bubbleChart = self._element
|
||||
bubbleChart._remove_bubbleScale()
|
||||
if value is None:
|
||||
return
|
||||
bubbleScale = bubbleChart._add_bubbleScale()
|
||||
bubbleScale.val = value
|
||||
|
||||
|
||||
class DoughnutPlot(_BasePlot):
|
||||
"""
|
||||
An doughnut plot.
|
||||
"""
|
||||
|
||||
|
||||
class LinePlot(_BasePlot):
|
||||
"""
|
||||
A line chart-style plot.
|
||||
"""
|
||||
|
||||
|
||||
class PiePlot(_BasePlot):
|
||||
"""
|
||||
A pie chart-style plot.
|
||||
"""
|
||||
|
||||
|
||||
class RadarPlot(_BasePlot):
|
||||
"""
|
||||
A radar-style plot.
|
||||
"""
|
||||
|
||||
|
||||
class XyPlot(_BasePlot):
|
||||
"""
|
||||
An XY (scatter) plot.
|
||||
"""
|
||||
|
||||
|
||||
def PlotFactory(xChart, chart):
|
||||
"""
|
||||
Return an instance of the appropriate subclass of _BasePlot based on the
|
||||
tagname of *xChart*.
|
||||
"""
|
||||
try:
|
||||
PlotCls = {
|
||||
qn("c:areaChart"): AreaPlot,
|
||||
qn("c:area3DChart"): Area3DPlot,
|
||||
qn("c:barChart"): BarPlot,
|
||||
qn("c:bubbleChart"): BubblePlot,
|
||||
qn("c:doughnutChart"): DoughnutPlot,
|
||||
qn("c:lineChart"): LinePlot,
|
||||
qn("c:pieChart"): PiePlot,
|
||||
qn("c:radarChart"): RadarPlot,
|
||||
qn("c:scatterChart"): XyPlot,
|
||||
}[xChart.tag]
|
||||
except KeyError:
|
||||
raise ValueError("unsupported plot type %s" % xChart.tag)
|
||||
|
||||
return PlotCls(xChart, chart)
|
||||
|
||||
|
||||
class PlotTypeInspector(object):
|
||||
"""
|
||||
"One-shot" service object that knows how to identify the type of a plot
|
||||
as a member of the XL_CHART_TYPE enumeration.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def chart_type(cls, plot):
|
||||
"""
|
||||
Return the member of :ref:`XlChartType` that corresponds to the chart
|
||||
type of *plot*.
|
||||
"""
|
||||
try:
|
||||
chart_type_method = {
|
||||
"AreaPlot": cls._differentiate_area_chart_type,
|
||||
"Area3DPlot": cls._differentiate_area_3d_chart_type,
|
||||
"BarPlot": cls._differentiate_bar_chart_type,
|
||||
"BubblePlot": cls._differentiate_bubble_chart_type,
|
||||
"DoughnutPlot": cls._differentiate_doughnut_chart_type,
|
||||
"LinePlot": cls._differentiate_line_chart_type,
|
||||
"PiePlot": cls._differentiate_pie_chart_type,
|
||||
"RadarPlot": cls._differentiate_radar_chart_type,
|
||||
"XyPlot": cls._differentiate_xy_chart_type,
|
||||
}[plot.__class__.__name__]
|
||||
except KeyError:
|
||||
raise NotImplementedError(
|
||||
"chart_type() not implemented for %s" % plot.__class__.__name__
|
||||
)
|
||||
return chart_type_method(plot)
|
||||
|
||||
@classmethod
|
||||
def _differentiate_area_3d_chart_type(cls, plot):
|
||||
return {
|
||||
ST_Grouping.STANDARD: XL.THREE_D_AREA,
|
||||
ST_Grouping.STACKED: XL.THREE_D_AREA_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.THREE_D_AREA_STACKED_100,
|
||||
}[plot._element.grouping_val]
|
||||
|
||||
@classmethod
|
||||
def _differentiate_area_chart_type(cls, plot):
|
||||
return {
|
||||
ST_Grouping.STANDARD: XL.AREA,
|
||||
ST_Grouping.STACKED: XL.AREA_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.AREA_STACKED_100,
|
||||
}[plot._element.grouping_val]
|
||||
|
||||
@classmethod
|
||||
def _differentiate_bar_chart_type(cls, plot):
|
||||
barChart = plot._element
|
||||
if barChart.barDir.val == ST_BarDir.BAR:
|
||||
return {
|
||||
ST_Grouping.CLUSTERED: XL.BAR_CLUSTERED,
|
||||
ST_Grouping.STACKED: XL.BAR_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.BAR_STACKED_100,
|
||||
}[barChart.grouping_val]
|
||||
if barChart.barDir.val == ST_BarDir.COL:
|
||||
return {
|
||||
ST_Grouping.CLUSTERED: XL.COLUMN_CLUSTERED,
|
||||
ST_Grouping.STACKED: XL.COLUMN_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.COLUMN_STACKED_100,
|
||||
}[barChart.grouping_val]
|
||||
raise ValueError("invalid barChart.barDir value '%s'" % barChart.barDir.val)
|
||||
|
||||
@classmethod
|
||||
def _differentiate_bubble_chart_type(cls, plot):
|
||||
def first_bubble3D(bubbleChart):
|
||||
results = bubbleChart.xpath("c:ser/c:bubble3D")
|
||||
return results[0] if results else None
|
||||
|
||||
bubbleChart = plot._element
|
||||
bubble3D = first_bubble3D(bubbleChart)
|
||||
|
||||
if bubble3D is None:
|
||||
return XL.BUBBLE
|
||||
if bubble3D.val:
|
||||
return XL.BUBBLE_THREE_D_EFFECT
|
||||
return XL.BUBBLE
|
||||
|
||||
@classmethod
|
||||
def _differentiate_doughnut_chart_type(cls, plot):
|
||||
doughnutChart = plot._element
|
||||
explosion = doughnutChart.xpath("./c:ser/c:explosion")
|
||||
return XL.DOUGHNUT_EXPLODED if explosion else XL.DOUGHNUT
|
||||
|
||||
@classmethod
|
||||
def _differentiate_line_chart_type(cls, plot):
|
||||
lineChart = plot._element
|
||||
|
||||
def has_line_markers():
|
||||
matches = lineChart.xpath('c:ser/c:marker/c:symbol[@val="none"]')
|
||||
if matches:
|
||||
return False
|
||||
return True
|
||||
|
||||
if has_line_markers():
|
||||
return {
|
||||
ST_Grouping.STANDARD: XL.LINE_MARKERS,
|
||||
ST_Grouping.STACKED: XL.LINE_MARKERS_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.LINE_MARKERS_STACKED_100,
|
||||
}[plot._element.grouping_val]
|
||||
else:
|
||||
return {
|
||||
ST_Grouping.STANDARD: XL.LINE,
|
||||
ST_Grouping.STACKED: XL.LINE_STACKED,
|
||||
ST_Grouping.PERCENT_STACKED: XL.LINE_STACKED_100,
|
||||
}[plot._element.grouping_val]
|
||||
|
||||
@classmethod
|
||||
def _differentiate_pie_chart_type(cls, plot):
|
||||
pieChart = plot._element
|
||||
explosion = pieChart.xpath("./c:ser/c:explosion")
|
||||
return XL.PIE_EXPLODED if explosion else XL.PIE
|
||||
|
||||
@classmethod
|
||||
def _differentiate_radar_chart_type(cls, plot):
|
||||
radarChart = plot._element
|
||||
radar_style = radarChart.xpath("c:radarStyle")[0].get("val")
|
||||
|
||||
def noMarkers():
|
||||
matches = radarChart.xpath("c:ser/c:marker/c:symbol")
|
||||
if matches and matches[0].get("val") == "none":
|
||||
return True
|
||||
return False
|
||||
|
||||
if radar_style is None:
|
||||
return XL.RADAR
|
||||
if radar_style == "filled":
|
||||
return XL.RADAR_FILLED
|
||||
if noMarkers():
|
||||
return XL.RADAR
|
||||
return XL.RADAR_MARKERS
|
||||
|
||||
@classmethod
|
||||
def _differentiate_xy_chart_type(cls, plot):
|
||||
scatterChart = plot._element
|
||||
|
||||
def noLine():
|
||||
return bool(scatterChart.xpath("c:ser/c:spPr/a:ln/a:noFill"))
|
||||
|
||||
def noMarkers():
|
||||
symbols = scatterChart.xpath("c:ser/c:marker/c:symbol")
|
||||
if symbols and symbols[0].get("val") == "none":
|
||||
return True
|
||||
return False
|
||||
|
||||
scatter_style = scatterChart.xpath("c:scatterStyle")[0].get("val")
|
||||
|
||||
if scatter_style == "lineMarker":
|
||||
if noLine():
|
||||
return XL.XY_SCATTER
|
||||
if noMarkers():
|
||||
return XL.XY_SCATTER_LINES_NO_MARKERS
|
||||
return XL.XY_SCATTER_LINES
|
||||
|
||||
if scatter_style == "smoothMarker":
|
||||
if noMarkers():
|
||||
return XL.XY_SCATTER_SMOOTH_NO_MARKERS
|
||||
return XL.XY_SCATTER_SMOOTH
|
||||
|
||||
return XL.XY_SCATTER
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Data point-related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pptx.chart.datalabel import DataLabel
|
||||
from pptx.chart.marker import Marker
|
||||
from pptx.dml.chtfmt import ChartFormat
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class _BasePoints(Sequence):
|
||||
"""
|
||||
Sequence providing access to the individual data points in a series.
|
||||
"""
|
||||
|
||||
def __init__(self, ser):
|
||||
super(_BasePoints, self).__init__()
|
||||
self._element = ser
|
||||
self._ser = ser
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx < 0 or idx >= self.__len__():
|
||||
raise IndexError("point index out of range")
|
||||
return Point(self._ser, idx)
|
||||
|
||||
|
||||
class BubblePoints(_BasePoints):
|
||||
"""
|
||||
Sequence providing access to the individual data points in
|
||||
a |BubbleSeries| object.
|
||||
"""
|
||||
|
||||
def __len__(self):
|
||||
return min(
|
||||
self._ser.xVal_ptCount_val,
|
||||
self._ser.yVal_ptCount_val,
|
||||
self._ser.bubbleSize_ptCount_val,
|
||||
)
|
||||
|
||||
|
||||
class CategoryPoints(_BasePoints):
|
||||
"""
|
||||
Sequence providing access to individual |Point| objects, each
|
||||
representing the visual properties of a data point in the specified
|
||||
category series.
|
||||
"""
|
||||
|
||||
def __len__(self):
|
||||
return self._ser.cat_ptCount_val
|
||||
|
||||
|
||||
class Point(object):
|
||||
"""
|
||||
Provides access to the properties of an individual data point in
|
||||
a series, such as the visual properties of its marker and the text and
|
||||
font of its data label.
|
||||
"""
|
||||
|
||||
def __init__(self, ser, idx):
|
||||
super(Point, self).__init__()
|
||||
self._element = ser
|
||||
self._ser = ser
|
||||
self._idx = idx
|
||||
|
||||
@lazyproperty
|
||||
def data_label(self):
|
||||
"""
|
||||
The |DataLabel| object representing the label on this data point.
|
||||
"""
|
||||
return DataLabel(self._ser, self._idx)
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""
|
||||
The |ChartFormat| object providing access to the shape formatting
|
||||
properties of this data point, such as line and fill.
|
||||
"""
|
||||
dPt = self._ser.get_or_add_dPt_for_point(self._idx)
|
||||
return ChartFormat(dPt)
|
||||
|
||||
@lazyproperty
|
||||
def marker(self):
|
||||
"""
|
||||
The |Marker| instance for this point, providing access to the visual
|
||||
properties of the data point marker, such as fill and line. Setting
|
||||
these properties overrides any value set at the series level.
|
||||
"""
|
||||
dPt = self._ser.get_or_add_dPt_for_point(self._idx)
|
||||
return Marker(dPt)
|
||||
|
||||
|
||||
class XyPoints(_BasePoints):
|
||||
"""
|
||||
Sequence providing access to the individual data points in an |XySeries|
|
||||
object.
|
||||
"""
|
||||
|
||||
def __len__(self):
|
||||
return min(self._ser.xVal_ptCount_val, self._ser.yVal_ptCount_val)
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Series-related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pptx.chart.datalabel import DataLabels
|
||||
from pptx.chart.marker import Marker
|
||||
from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints
|
||||
from pptx.dml.chtfmt import ChartFormat
|
||||
from pptx.oxml.ns import qn
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class _BaseSeries(object):
|
||||
"""
|
||||
Base class for |BarSeries| and other series classes.
|
||||
"""
|
||||
|
||||
def __init__(self, ser):
|
||||
super(_BaseSeries, self).__init__()
|
||||
self._element = ser
|
||||
self._ser = ser
|
||||
|
||||
@lazyproperty
|
||||
def format(self):
|
||||
"""
|
||||
The |ChartFormat| instance for this series, providing access to shape
|
||||
properties such as fill and line.
|
||||
"""
|
||||
return ChartFormat(self._ser)
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""
|
||||
The zero-based integer index of this series as reported in its
|
||||
`c:ser/c:idx` element.
|
||||
"""
|
||||
return self._element.idx.val
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
The string label given to this series, appears as the title of the
|
||||
column for this series in the Excel worksheet. It also appears as the
|
||||
label for this series in the legend.
|
||||
"""
|
||||
names = self._element.xpath("./c:tx//c:pt/c:v/text()")
|
||||
name = names[0] if names else ""
|
||||
return name
|
||||
|
||||
|
||||
class _BaseCategorySeries(_BaseSeries):
|
||||
"""Base class for |BarSeries| and other category chart series classes."""
|
||||
|
||||
@lazyproperty
|
||||
def data_labels(self):
|
||||
"""|DataLabels| object controlling data labels for this series."""
|
||||
return DataLabels(self._ser.get_or_add_dLbls())
|
||||
|
||||
@lazyproperty
|
||||
def points(self):
|
||||
"""
|
||||
The |CategoryPoints| object providing access to individual data
|
||||
points in this series.
|
||||
"""
|
||||
return CategoryPoints(self._ser)
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
"""
|
||||
Read-only. A sequence containing the float values for this series, in
|
||||
the order they appear on the chart.
|
||||
"""
|
||||
|
||||
def iter_values():
|
||||
val = self._element.val
|
||||
if val is None:
|
||||
return
|
||||
for idx in range(val.ptCount_val):
|
||||
yield val.pt_v(idx)
|
||||
|
||||
return tuple(iter_values())
|
||||
|
||||
|
||||
class _MarkerMixin(object):
|
||||
"""
|
||||
Mixin class providing `.marker` property for line-type chart series. The
|
||||
line-type charts are Line, XY, and Radar.
|
||||
"""
|
||||
|
||||
@lazyproperty
|
||||
def marker(self):
|
||||
"""
|
||||
The |Marker| instance for this series, providing access to data point
|
||||
marker properties such as fill and line. Setting these properties
|
||||
determines the appearance of markers for all points in this series
|
||||
that are not overridden by settings at the point level.
|
||||
"""
|
||||
return Marker(self._ser)
|
||||
|
||||
|
||||
class AreaSeries(_BaseCategorySeries):
|
||||
"""
|
||||
A data point series belonging to an area plot.
|
||||
"""
|
||||
|
||||
|
||||
class BarSeries(_BaseCategorySeries):
|
||||
"""A data point series belonging to a bar plot."""
|
||||
|
||||
@property
|
||||
def invert_if_negative(self):
|
||||
"""
|
||||
|True| if a point having a value less than zero should appear with a
|
||||
fill different than those with a positive value. |False| if the fill
|
||||
should be the same regardless of the bar's value. When |True|, a bar
|
||||
with a solid fill appears with white fill; in a bar with gradient
|
||||
fill, the direction of the gradient is reversed, e.g. dark -> light
|
||||
instead of light -> dark. The term "invert" here should be understood
|
||||
to mean "invert the *direction* of the *fill gradient*".
|
||||
"""
|
||||
invertIfNegative = self._element.invertIfNegative
|
||||
if invertIfNegative is None:
|
||||
return True
|
||||
return invertIfNegative.val
|
||||
|
||||
@invert_if_negative.setter
|
||||
def invert_if_negative(self, value):
|
||||
invertIfNegative = self._element.get_or_add_invertIfNegative()
|
||||
invertIfNegative.val = value
|
||||
|
||||
|
||||
class LineSeries(_BaseCategorySeries, _MarkerMixin):
|
||||
"""
|
||||
A data point series belonging to a line plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
def smooth(self):
|
||||
"""
|
||||
Read/write boolean specifying whether to use curve smoothing to
|
||||
form the line connecting the data points in this series into
|
||||
a continuous curve. If |False|, a series of straight line segments
|
||||
are used to connect the points.
|
||||
"""
|
||||
smooth = self._element.smooth
|
||||
if smooth is None:
|
||||
return True
|
||||
return smooth.val
|
||||
|
||||
@smooth.setter
|
||||
def smooth(self, value):
|
||||
self._element.get_or_add_smooth().val = value
|
||||
|
||||
|
||||
class PieSeries(_BaseCategorySeries):
|
||||
"""
|
||||
A data point series belonging to a pie plot.
|
||||
"""
|
||||
|
||||
|
||||
class RadarSeries(_BaseCategorySeries, _MarkerMixin):
|
||||
"""
|
||||
A data point series belonging to a radar plot.
|
||||
"""
|
||||
|
||||
|
||||
class XySeries(_BaseSeries, _MarkerMixin):
|
||||
"""
|
||||
A data point series belonging to an XY (scatter) plot.
|
||||
"""
|
||||
|
||||
def iter_values(self):
|
||||
"""
|
||||
Generate each float Y value in this series, in the order they appear
|
||||
on the chart. A value of `None` represents a missing Y value
|
||||
(corresponding to a blank Excel cell).
|
||||
"""
|
||||
yVal = self._element.yVal
|
||||
if yVal is None:
|
||||
return
|
||||
|
||||
for idx in range(yVal.ptCount_val):
|
||||
yield yVal.pt_v(idx)
|
||||
|
||||
@lazyproperty
|
||||
def points(self):
|
||||
"""
|
||||
The |XyPoints| object providing access to individual data points in
|
||||
this series.
|
||||
"""
|
||||
return XyPoints(self._ser)
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
"""
|
||||
Read-only. A sequence containing the float values for this series, in
|
||||
the order they appear on the chart.
|
||||
"""
|
||||
return tuple(self.iter_values())
|
||||
|
||||
|
||||
class BubbleSeries(XySeries):
|
||||
"""
|
||||
A data point series belonging to a bubble plot.
|
||||
"""
|
||||
|
||||
@lazyproperty
|
||||
def points(self):
|
||||
"""
|
||||
The |BubblePoints| object providing access to individual data point
|
||||
objects used to discover and adjust the formatting and data labels of
|
||||
a data point.
|
||||
"""
|
||||
return BubblePoints(self._ser)
|
||||
|
||||
|
||||
class SeriesCollection(Sequence):
|
||||
"""
|
||||
A sequence of |Series| objects.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_elm):
|
||||
# *parent_elm* can be either a c:plotArea or xChart element
|
||||
super(SeriesCollection, self).__init__()
|
||||
self._element = parent_elm
|
||||
|
||||
def __getitem__(self, index):
|
||||
ser = self._element.sers[index]
|
||||
return _SeriesFactory(ser)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._element.sers)
|
||||
|
||||
|
||||
def _SeriesFactory(ser):
|
||||
"""
|
||||
Return an instance of the appropriate subclass of _BaseSeries based on the
|
||||
xChart element *ser* appears in.
|
||||
"""
|
||||
xChart_tag = ser.getparent().tag
|
||||
|
||||
try:
|
||||
SeriesCls = {
|
||||
qn("c:areaChart"): AreaSeries,
|
||||
qn("c:barChart"): BarSeries,
|
||||
qn("c:bubbleChart"): BubbleSeries,
|
||||
qn("c:doughnutChart"): PieSeries,
|
||||
qn("c:lineChart"): LineSeries,
|
||||
qn("c:pieChart"): PieSeries,
|
||||
qn("c:radarChart"): RadarSeries,
|
||||
qn("c:scatterChart"): XySeries,
|
||||
}[xChart_tag]
|
||||
except KeyError:
|
||||
raise NotImplementedError("series class for %s not yet implemented" % xChart_tag)
|
||||
|
||||
return SeriesCls(ser)
|
||||
@@ -0,0 +1,272 @@
|
||||
"""Chart builder and related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from contextlib import contextmanager
|
||||
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
|
||||
class _BaseWorkbookWriter(object):
|
||||
"""Base class for workbook writers, providing shared members."""
|
||||
|
||||
def __init__(self, chart_data):
|
||||
super(_BaseWorkbookWriter, self).__init__()
|
||||
self._chart_data = chart_data
|
||||
|
||||
@property
|
||||
def xlsx_blob(self):
|
||||
"""bytes for Excel file containing chart_data."""
|
||||
xlsx_file = io.BytesIO()
|
||||
with self._open_worksheet(xlsx_file) as (workbook, worksheet):
|
||||
self._populate_worksheet(workbook, worksheet)
|
||||
return xlsx_file.getvalue()
|
||||
|
||||
@contextmanager
|
||||
def _open_worksheet(self, xlsx_file):
|
||||
"""
|
||||
Enable XlsxWriter Worksheet object to be opened, operated on, and
|
||||
then automatically closed within a `with` statement. A filename or
|
||||
stream object (such as an `io.BytesIO` instance) is expected as
|
||||
*xlsx_file*.
|
||||
"""
|
||||
workbook = Workbook(xlsx_file, {"in_memory": True})
|
||||
worksheet = workbook.add_worksheet()
|
||||
yield workbook, worksheet
|
||||
workbook.close()
|
||||
|
||||
def _populate_worksheet(self, workbook, worksheet):
|
||||
"""
|
||||
Must be overridden by each subclass to provide the particulars of
|
||||
writing the spreadsheet data.
|
||||
"""
|
||||
raise NotImplementedError("must be provided by each subclass")
|
||||
|
||||
|
||||
class CategoryWorkbookWriter(_BaseWorkbookWriter):
|
||||
"""
|
||||
Determines Excel worksheet layout and can write an Excel workbook from
|
||||
a CategoryChartData object. Serves as the authority for Excel worksheet
|
||||
ranges.
|
||||
"""
|
||||
|
||||
@property
|
||||
def categories_ref(self):
|
||||
"""
|
||||
The Excel worksheet reference to the categories for this chart (not
|
||||
including the column heading).
|
||||
"""
|
||||
categories = self._chart_data.categories
|
||||
if categories.depth == 0:
|
||||
raise ValueError("chart data contains no categories")
|
||||
right_col = chr(ord("A") + categories.depth - 1)
|
||||
bottom_row = categories.leaf_count + 1
|
||||
return "Sheet1!$A$2:$%s$%d" % (right_col, bottom_row)
|
||||
|
||||
def series_name_ref(self, series):
|
||||
"""
|
||||
Return the Excel worksheet reference to the cell containing the name
|
||||
for *series*. This also serves as the column heading for the series
|
||||
values.
|
||||
"""
|
||||
return "Sheet1!$%s$1" % self._series_col_letter(series)
|
||||
|
||||
def values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the values for this series (not
|
||||
including the column heading).
|
||||
"""
|
||||
return "Sheet1!${col_letter}$2:${col_letter}${bottom_row}".format(
|
||||
**{
|
||||
"col_letter": self._series_col_letter(series),
|
||||
"bottom_row": len(series) + 1,
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _column_reference(column_number):
|
||||
"""Return str Excel column reference like 'BQ' for *column_number*.
|
||||
|
||||
*column_number* is an int in the range 1-16384 inclusive, where
|
||||
1 maps to column 'A'.
|
||||
"""
|
||||
if column_number < 1 or column_number > 16384:
|
||||
raise ValueError("column_number must be in range 1-16384")
|
||||
|
||||
# ---Work right-to-left, one order of magnitude at a time. Note there
|
||||
# is no zero representation in Excel address scheme, so this is
|
||||
# not just a conversion to base-26---
|
||||
|
||||
col_ref = ""
|
||||
while column_number:
|
||||
remainder = column_number % 26
|
||||
if remainder == 0:
|
||||
remainder = 26
|
||||
|
||||
col_letter = chr(ord("A") + remainder - 1)
|
||||
col_ref = col_letter + col_ref
|
||||
|
||||
# ---Advance to next order of magnitude or terminate loop. The
|
||||
# minus-one in this expression reflects the fact the next lower
|
||||
# order of magnitude has a minumum value of 1 (not zero). This is
|
||||
# essentially the complement to the "if it's 0 make it 26' step
|
||||
# above.---
|
||||
column_number = (column_number - 1) // 26
|
||||
|
||||
return col_ref
|
||||
|
||||
def _populate_worksheet(self, workbook, worksheet):
|
||||
"""
|
||||
Write the chart data contents to *worksheet* in category chart
|
||||
layout. Write categories starting in the first column starting in
|
||||
the second row, and proceeding one column per category level (for
|
||||
charts having multi-level categories). Write series as columns
|
||||
starting in the next following column, placing the series title in
|
||||
the first cell.
|
||||
"""
|
||||
self._write_categories(workbook, worksheet)
|
||||
self._write_series(workbook, worksheet)
|
||||
|
||||
def _series_col_letter(self, series):
|
||||
"""
|
||||
The letter of the Excel worksheet column in which the data for a
|
||||
series appears.
|
||||
"""
|
||||
column_number = 1 + series.categories.depth + series.index
|
||||
return self._column_reference(column_number)
|
||||
|
||||
def _write_categories(self, workbook, worksheet):
|
||||
"""
|
||||
Write the categories column(s) to *worksheet*. Categories start in
|
||||
the first column starting in the second row, and proceeding one
|
||||
column per category level (for charts having multi-level categories).
|
||||
A date category is formatted as a date. All others are formatted
|
||||
`General`.
|
||||
"""
|
||||
categories = self._chart_data.categories
|
||||
num_format = workbook.add_format({"num_format": categories.number_format})
|
||||
depth = categories.depth
|
||||
for idx, level in enumerate(categories.levels):
|
||||
col = depth - idx - 1
|
||||
self._write_cat_column(worksheet, col, level, num_format)
|
||||
|
||||
def _write_cat_column(self, worksheet, col, level, num_format):
|
||||
"""
|
||||
Write a category column defined by *level* to *worksheet* at offset
|
||||
*col* and formatted with *num_format*.
|
||||
"""
|
||||
worksheet.set_column(col, col, 10) # wide enough for a date
|
||||
for off, name in level:
|
||||
row = off + 1
|
||||
worksheet.write(row, col, name, num_format)
|
||||
|
||||
def _write_series(self, workbook, worksheet):
|
||||
"""
|
||||
Write the series column(s) to *worksheet*. Series start in the column
|
||||
following the last categories column, placing the series title in the
|
||||
first cell.
|
||||
"""
|
||||
col_offset = self._chart_data.categories.depth
|
||||
for idx, series in enumerate(self._chart_data):
|
||||
num_format = workbook.add_format({"num_format": series.number_format})
|
||||
series_col = idx + col_offset
|
||||
worksheet.write(0, series_col, series.name)
|
||||
worksheet.write_column(1, series_col, series.values, num_format)
|
||||
|
||||
|
||||
class XyWorkbookWriter(_BaseWorkbookWriter):
|
||||
"""
|
||||
Determines Excel worksheet layout and can write an Excel workbook from XY
|
||||
chart data. Serves as the authority for Excel worksheet ranges.
|
||||
"""
|
||||
|
||||
def series_name_ref(self, series):
|
||||
"""
|
||||
Return the Excel worksheet reference to the cell containing the name
|
||||
for *series*. This also serves as the column heading for the series
|
||||
Y values.
|
||||
"""
|
||||
row = self.series_table_row_offset(series) + 1
|
||||
return "Sheet1!$B$%d" % row
|
||||
|
||||
def series_table_row_offset(self, series):
|
||||
"""
|
||||
Return the number of rows preceding the data table for *series* in
|
||||
the Excel worksheet.
|
||||
"""
|
||||
title_and_spacer_rows = series.index * 2
|
||||
data_point_rows = series.data_point_offset
|
||||
return title_and_spacer_rows + data_point_rows
|
||||
|
||||
def x_values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the X values for this chart (not
|
||||
including the column label).
|
||||
"""
|
||||
top_row = self.series_table_row_offset(series) + 2
|
||||
bottom_row = top_row + len(series) - 1
|
||||
return "Sheet1!$A$%d:$A$%d" % (top_row, bottom_row)
|
||||
|
||||
def y_values_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the Y values for this chart (not
|
||||
including the column label).
|
||||
"""
|
||||
top_row = self.series_table_row_offset(series) + 2
|
||||
bottom_row = top_row + len(series) - 1
|
||||
return "Sheet1!$B$%d:$B$%d" % (top_row, bottom_row)
|
||||
|
||||
def _populate_worksheet(self, workbook, worksheet):
|
||||
"""
|
||||
Write chart data contents to *worksheet* in the standard XY chart
|
||||
layout. Write the data for each series to a separate two-column
|
||||
table, X values in column A and Y values in column B. Place the
|
||||
series label in the first (heading) cell of the column.
|
||||
"""
|
||||
chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
|
||||
for series in self._chart_data:
|
||||
series_num_format = workbook.add_format({"num_format": series.number_format})
|
||||
offset = self.series_table_row_offset(series)
|
||||
# write X values
|
||||
worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
|
||||
# write Y values
|
||||
worksheet.write(offset, 1, series.name)
|
||||
worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
|
||||
|
||||
|
||||
class BubbleWorkbookWriter(XyWorkbookWriter):
|
||||
"""
|
||||
Service object that knows how to write an Excel workbook from bubble
|
||||
chart data.
|
||||
"""
|
||||
|
||||
def bubble_sizes_ref(self, series):
|
||||
"""
|
||||
The Excel worksheet reference to the range containing the bubble
|
||||
sizes for *series* (not including the column heading cell).
|
||||
"""
|
||||
top_row = self.series_table_row_offset(series) + 2
|
||||
bottom_row = top_row + len(series) - 1
|
||||
return "Sheet1!$C$%d:$C$%d" % (top_row, bottom_row)
|
||||
|
||||
def _populate_worksheet(self, workbook, worksheet):
|
||||
"""
|
||||
Write chart data contents to *worksheet* in the bubble chart layout.
|
||||
Write the data for each series to a separate three-column table with
|
||||
X values in column A, Y values in column B, and bubble sizes in
|
||||
column C. Place the series label in the first (heading) cell of the
|
||||
values column.
|
||||
"""
|
||||
chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
|
||||
for series in self._chart_data:
|
||||
series_num_format = workbook.add_format({"num_format": series.number_format})
|
||||
offset = self.series_table_row_offset(series)
|
||||
# write X values
|
||||
worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
|
||||
# write Y values
|
||||
worksheet.write(offset, 1, series.name)
|
||||
worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
|
||||
# write bubble sizes
|
||||
worksheet.write(offset, 2, "Size")
|
||||
worksheet.write_column(offset + 1, 2, series.bubble_sizes, chart_num_format)
|
||||
File diff suppressed because it is too large
Load Diff
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,40 @@
|
||||
"""|ChartFormat| and related objects.
|
||||
|
||||
|ChartFormat| acts as proxy for the `spPr` element, which provides visual shape properties such as
|
||||
line and fill for chart elements.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.dml.fill import FillFormat
|
||||
from pptx.dml.line import LineFormat
|
||||
from pptx.shared import ElementProxy
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class ChartFormat(ElementProxy):
|
||||
"""
|
||||
The |ChartFormat| object provides access to visual shape properties for
|
||||
chart elements like |Axis|, |Series|, and |MajorGridlines|. It has two
|
||||
properties, :attr:`fill` and :attr:`line`, which return a |FillFormat|
|
||||
and |LineFormat| object respectively. The |ChartFormat| object is
|
||||
provided by the :attr:`format` property on the target axis, series, etc.
|
||||
"""
|
||||
|
||||
@lazyproperty
|
||||
def fill(self):
|
||||
"""
|
||||
|FillFormat| instance for this object, providing access to fill
|
||||
properties such as fill color.
|
||||
"""
|
||||
spPr = self._element.get_or_add_spPr()
|
||||
return FillFormat.from_fill_parent(spPr)
|
||||
|
||||
@lazyproperty
|
||||
def line(self):
|
||||
"""
|
||||
The |LineFormat| object providing access to the visual properties of
|
||||
this object, such as line color and line style.
|
||||
"""
|
||||
spPr = self._element.get_or_add_spPr()
|
||||
return LineFormat(spPr)
|
||||
@@ -0,0 +1,301 @@
|
||||
"""DrawingML objects related to color, ColorFormat being the most prominent."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR
|
||||
from pptx.oxml.dml.color import (
|
||||
CT_HslColor,
|
||||
CT_PresetColor,
|
||||
CT_SchemeColor,
|
||||
CT_ScRgbColor,
|
||||
CT_SRgbColor,
|
||||
CT_SystemColor,
|
||||
)
|
||||
|
||||
|
||||
class ColorFormat(object):
|
||||
"""
|
||||
Provides access to color settings such as RGB color, theme color, and
|
||||
luminance adjustments.
|
||||
"""
|
||||
|
||||
def __init__(self, eg_colorChoice_parent, color):
|
||||
super(ColorFormat, self).__init__()
|
||||
self._xFill = eg_colorChoice_parent
|
||||
self._color = color
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""
|
||||
Read/write float value between -1.0 and 1.0 indicating the brightness
|
||||
adjustment for this color, e.g. -0.25 is 25% darker and 0.4 is 40%
|
||||
lighter. 0 means no brightness adjustment.
|
||||
"""
|
||||
return self._color.brightness
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, value):
|
||||
self._validate_brightness_value(value)
|
||||
self._color.brightness = value
|
||||
|
||||
@classmethod
|
||||
def from_colorchoice_parent(cls, eg_colorChoice_parent):
|
||||
xClr = eg_colorChoice_parent.eg_colorChoice
|
||||
color = _Color(xClr)
|
||||
color_format = cls(eg_colorChoice_parent, color)
|
||||
return color_format
|
||||
|
||||
@property
|
||||
def rgb(self):
|
||||
"""
|
||||
|RGBColor| value of this color, or None if no RGB color is explicitly
|
||||
defined for this font. Setting this value to an |RGBColor| instance
|
||||
causes its type to change to MSO_COLOR_TYPE.RGB. If the color was a
|
||||
theme color with a brightness adjustment, the brightness adjustment
|
||||
is removed when changing it to an RGB color.
|
||||
"""
|
||||
return self._color.rgb
|
||||
|
||||
@rgb.setter
|
||||
def rgb(self, rgb):
|
||||
if not isinstance(rgb, RGBColor):
|
||||
raise ValueError("assigned value must be type RGBColor")
|
||||
# change to rgb color format if not already
|
||||
if not isinstance(self._color, _SRgbColor):
|
||||
srgbClr = self._xFill.get_or_change_to_srgbClr()
|
||||
self._color = _SRgbColor(srgbClr)
|
||||
# call _SRgbColor instance to do the setting
|
||||
self._color.rgb = rgb
|
||||
|
||||
@property
|
||||
def theme_color(self):
|
||||
"""Theme color value of this color.
|
||||
|
||||
Value is a member of :ref:`MsoThemeColorIndex`, e.g.
|
||||
``MSO_THEME_COLOR.ACCENT_1``. Raises AttributeError on access if the
|
||||
color is not type ``MSO_COLOR_TYPE.SCHEME``. Assigning a member of
|
||||
:ref:`MsoThemeColorIndex` causes the color's type to change to
|
||||
``MSO_COLOR_TYPE.SCHEME``.
|
||||
"""
|
||||
return self._color.theme_color
|
||||
|
||||
@theme_color.setter
|
||||
def theme_color(self, mso_theme_color_idx):
|
||||
# change to theme color format if not already
|
||||
if not isinstance(self._color, _SchemeColor):
|
||||
schemeClr = self._xFill.get_or_change_to_schemeClr()
|
||||
self._color = _SchemeColor(schemeClr)
|
||||
self._color.theme_color = mso_theme_color_idx
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""
|
||||
Read-only. A value from :ref:`MsoColorType`, either RGB or SCHEME,
|
||||
corresponding to the way this color is defined, or None if no color
|
||||
is defined at the level of this font.
|
||||
"""
|
||||
return self._color.color_type
|
||||
|
||||
def _validate_brightness_value(self, value):
|
||||
if value < -1.0 or value > 1.0:
|
||||
raise ValueError("brightness must be number in range -1.0 to 1.0")
|
||||
if isinstance(self._color, _NoneColor):
|
||||
msg = (
|
||||
"can't set brightness when color.type is None. Set color.rgb"
|
||||
" or .theme_color first."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
class _Color(object):
|
||||
"""
|
||||
Object factory for color object of the appropriate type, also the base
|
||||
class for all color type classes such as SRgbColor.
|
||||
"""
|
||||
|
||||
def __new__(cls, xClr):
|
||||
color_cls = {
|
||||
type(None): _NoneColor,
|
||||
CT_HslColor: _HslColor,
|
||||
CT_PresetColor: _PrstColor,
|
||||
CT_SchemeColor: _SchemeColor,
|
||||
CT_ScRgbColor: _ScRgbColor,
|
||||
CT_SRgbColor: _SRgbColor,
|
||||
CT_SystemColor: _SysColor,
|
||||
}[type(xClr)]
|
||||
return super(_Color, cls).__new__(color_cls)
|
||||
|
||||
def __init__(self, xClr):
|
||||
super(_Color, self).__init__()
|
||||
self._xClr = xClr
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
lumMod, lumOff = self._xClr.lumMod, self._xClr.lumOff
|
||||
# a tint is lighter, a shade is darker
|
||||
# only tints have lumOff child
|
||||
if lumOff is not None:
|
||||
brightness = lumOff.val
|
||||
return brightness
|
||||
# which leaves shades, if lumMod is present
|
||||
if lumMod is not None:
|
||||
brightness = lumMod.val - 1.0
|
||||
return brightness
|
||||
# there's no brightness adjustment if no lum{Mod|Off} elements
|
||||
return 0
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, value):
|
||||
if value > 0:
|
||||
self._tint(value)
|
||||
elif value < 0:
|
||||
self._shade(value)
|
||||
else:
|
||||
self._xClr.clear_lum()
|
||||
|
||||
@property
|
||||
def color_type(self): # pragma: no cover
|
||||
tmpl = ".color_type property must be implemented on %s"
|
||||
raise NotImplementedError(tmpl % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def rgb(self):
|
||||
"""
|
||||
Raises TypeError on access unless overridden by subclass.
|
||||
"""
|
||||
tmpl = "no .rgb property on color type '%s'"
|
||||
raise AttributeError(tmpl % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def theme_color(self):
|
||||
"""
|
||||
Raises TypeError on access unless overridden by subclass.
|
||||
"""
|
||||
return MSO_THEME_COLOR.NOT_THEME_COLOR
|
||||
|
||||
def _shade(self, value):
|
||||
lumMod_val = 1.0 - abs(value)
|
||||
color_elm = self._xClr.clear_lum()
|
||||
color_elm.add_lumMod(lumMod_val)
|
||||
|
||||
def _tint(self, value):
|
||||
lumOff_val = value
|
||||
lumMod_val = 1.0 - lumOff_val
|
||||
color_elm = self._xClr.clear_lum()
|
||||
color_elm.add_lumMod(lumMod_val)
|
||||
color_elm.add_lumOff(lumOff_val)
|
||||
|
||||
|
||||
class _HslColor(_Color):
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.HSL
|
||||
|
||||
|
||||
class _NoneColor(_Color):
|
||||
@property
|
||||
def color_type(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def theme_color(self):
|
||||
"""
|
||||
Raise TypeError on attempt to access .theme_color when no color
|
||||
choice is present.
|
||||
"""
|
||||
tmpl = "no .theme_color property on color type '%s'"
|
||||
raise AttributeError(tmpl % self.__class__.__name__)
|
||||
|
||||
|
||||
class _PrstColor(_Color):
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.PRESET
|
||||
|
||||
|
||||
class _SchemeColor(_Color):
|
||||
def __init__(self, schemeClr):
|
||||
super(_SchemeColor, self).__init__(schemeClr)
|
||||
self._schemeClr = schemeClr
|
||||
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.SCHEME
|
||||
|
||||
@property
|
||||
def theme_color(self):
|
||||
"""
|
||||
Theme color value of this color, one of those defined in the
|
||||
MSO_THEME_COLOR enumeration, e.g. MSO_THEME_COLOR.ACCENT_1. None if
|
||||
no theme color is explicitly defined for this font. Setting this to a
|
||||
value in MSO_THEME_COLOR causes the color's type to change to
|
||||
``MSO_COLOR_TYPE.SCHEME``.
|
||||
"""
|
||||
return self._schemeClr.val
|
||||
|
||||
@theme_color.setter
|
||||
def theme_color(self, mso_theme_color_idx):
|
||||
self._schemeClr.val = mso_theme_color_idx
|
||||
|
||||
|
||||
class _ScRgbColor(_Color):
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.SCRGB
|
||||
|
||||
|
||||
class _SRgbColor(_Color):
|
||||
def __init__(self, srgbClr):
|
||||
super(_SRgbColor, self).__init__(srgbClr)
|
||||
self._srgbClr = srgbClr
|
||||
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.RGB
|
||||
|
||||
@property
|
||||
def rgb(self):
|
||||
"""
|
||||
|RGBColor| value of this color, corresponding to the value in the
|
||||
required ``val`` attribute of the ``<a:srgbColr>`` element.
|
||||
"""
|
||||
return RGBColor.from_string(self._srgbClr.val)
|
||||
|
||||
@rgb.setter
|
||||
def rgb(self, rgb):
|
||||
self._srgbClr.val = str(rgb)
|
||||
|
||||
|
||||
class _SysColor(_Color):
|
||||
@property
|
||||
def color_type(self):
|
||||
return MSO_COLOR_TYPE.SYSTEM
|
||||
|
||||
|
||||
class RGBColor(tuple):
|
||||
"""
|
||||
Immutable value object defining a particular RGB color.
|
||||
"""
|
||||
|
||||
def __new__(cls, r, g, b):
|
||||
msg = "RGBColor() takes three integer values 0-255"
|
||||
for val in (r, g, b):
|
||||
if not isinstance(val, int) or val < 0 or val > 255:
|
||||
raise ValueError(msg)
|
||||
return super(RGBColor, cls).__new__(cls, (r, g, b))
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return a hex string rgb value, like '3C2F80'
|
||||
"""
|
||||
return "%02X%02X%02X" % self
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, rgb_hex_str):
|
||||
"""
|
||||
Return a new instance from an RGB color hex string like ``'3C2F80'``.
|
||||
"""
|
||||
r = int(rgb_hex_str[:2], 16)
|
||||
g = int(rgb_hex_str[2:4], 16)
|
||||
b = int(rgb_hex_str[4:], 16)
|
||||
return cls(r, g, b)
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Visual effects on a shape such as shadow, glow, and reflection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class ShadowFormat(object):
|
||||
"""Provides access to shadow effect on a shape."""
|
||||
|
||||
def __init__(self, spPr):
|
||||
# ---spPr may also be a grpSpPr; both have a:effectLst child---
|
||||
self._element = spPr
|
||||
|
||||
@property
|
||||
def inherit(self):
|
||||
"""True if shape inherits shadow settings.
|
||||
|
||||
Read/write. An explicitly-defined shadow setting on a shape causes
|
||||
this property to return |False|. A shape with no explicitly-defined
|
||||
shadow setting inherits its shadow settings from the style hierarchy
|
||||
(and so returns |True|).
|
||||
|
||||
Assigning |True| causes any explicitly-defined shadow setting to be
|
||||
removed and inheritance is restored. Note this has the side-effect of
|
||||
removing **all** explicitly-defined effects, such as glow and
|
||||
reflection, and restoring inheritance for all effects on the shape.
|
||||
Assigning |False| causes the inheritance link to be broken and **no**
|
||||
effects to appear on the shape.
|
||||
"""
|
||||
if self._element.effectLst is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
@inherit.setter
|
||||
def inherit(self, value):
|
||||
inherit = bool(value)
|
||||
if inherit:
|
||||
# ---remove any explicitly-defined effects
|
||||
self._element._remove_effectLst()
|
||||
else:
|
||||
# ---ensure at least the effectLst element is present
|
||||
self._element.get_or_add_effectLst()
|
||||
@@ -0,0 +1,398 @@
|
||||
"""DrawingML objects related to fill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pptx.dml.color import ColorFormat
|
||||
from pptx.enum.dml import MSO_FILL
|
||||
from pptx.oxml.dml.fill import (
|
||||
CT_BlipFillProperties,
|
||||
CT_GradientFillProperties,
|
||||
CT_GroupFillProperties,
|
||||
CT_NoFillProperties,
|
||||
CT_PatternFillProperties,
|
||||
CT_SolidColorFillProperties,
|
||||
)
|
||||
from pptx.oxml.xmlchemy import BaseOxmlElement
|
||||
from pptx.shared import ElementProxy
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx.enum.dml import MSO_FILL_TYPE
|
||||
from pptx.oxml.xmlchemy import BaseOxmlElement
|
||||
|
||||
|
||||
class FillFormat(object):
|
||||
"""Provides access to the current fill properties.
|
||||
|
||||
Also provides methods to change the fill type.
|
||||
"""
|
||||
|
||||
def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill):
|
||||
super(FillFormat, self).__init__()
|
||||
self._xPr = eg_fill_properties_parent
|
||||
self._fill = fill_obj
|
||||
|
||||
@classmethod
|
||||
def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat:
|
||||
"""
|
||||
Return a |FillFormat| instance initialized to the settings contained
|
||||
in *eg_fillProperties_parent*, which must be an element having
|
||||
EG_FillProperties in its child element sequence in the XML schema.
|
||||
"""
|
||||
fill_elm = eg_fillProperties_parent.eg_fillProperties
|
||||
fill = _Fill(fill_elm)
|
||||
fill_format = cls(eg_fillProperties_parent, fill)
|
||||
return fill_format
|
||||
|
||||
@property
|
||||
def back_color(self):
|
||||
"""Return a |ColorFormat| object representing background color.
|
||||
|
||||
This property is only applicable to pattern fills and lines.
|
||||
"""
|
||||
return self._fill.back_color
|
||||
|
||||
def background(self):
|
||||
"""
|
||||
Sets the fill type to noFill, i.e. transparent.
|
||||
"""
|
||||
noFill = self._xPr.get_or_change_to_noFill()
|
||||
self._fill = _NoFill(noFill)
|
||||
|
||||
@property
|
||||
def fore_color(self):
|
||||
"""
|
||||
Return a |ColorFormat| instance representing the foreground color of
|
||||
this fill.
|
||||
"""
|
||||
return self._fill.fore_color
|
||||
|
||||
def gradient(self):
|
||||
"""Sets the fill type to gradient.
|
||||
|
||||
If the fill is not already a gradient, a default gradient is added.
|
||||
The default gradient corresponds to the default in the built-in
|
||||
PowerPoint "White" template. This gradient is linear at angle
|
||||
90-degrees (upward), with two stops. The first stop is Accent-1 with
|
||||
tint 100%, shade 100%, and satMod 130%. The second stop is Accent-1
|
||||
with tint 50%, shade 100%, and satMod 350%.
|
||||
"""
|
||||
gradFill = self._xPr.get_or_change_to_gradFill()
|
||||
self._fill = _GradFill(gradFill)
|
||||
|
||||
@property
|
||||
def gradient_angle(self):
|
||||
"""Angle in float degrees of line of a linear gradient.
|
||||
|
||||
Read/Write. May be |None|, indicating the angle should be inherited
|
||||
from the style hierarchy. An angle of 0.0 corresponds to
|
||||
a left-to-right gradient. Increasing angles represent
|
||||
counter-clockwise rotation of the line, for example 90.0 represents
|
||||
a bottom-to-top gradient. Raises |TypeError| when the fill type is
|
||||
not MSO_FILL_TYPE.GRADIENT. Raises |ValueError| for a non-linear
|
||||
gradient (e.g. a radial gradient).
|
||||
"""
|
||||
if self.type != MSO_FILL.GRADIENT:
|
||||
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
||||
return self._fill.gradient_angle
|
||||
|
||||
@gradient_angle.setter
|
||||
def gradient_angle(self, value):
|
||||
if self.type != MSO_FILL.GRADIENT:
|
||||
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
||||
self._fill.gradient_angle = value
|
||||
|
||||
@property
|
||||
def gradient_stops(self):
|
||||
"""|GradientStops| object providing access to stops of this gradient.
|
||||
|
||||
Raises |TypeError| when fill is not gradient (call `fill.gradient()`
|
||||
first). Each stop represents a color between which the gradient
|
||||
smoothly transitions.
|
||||
"""
|
||||
if self.type != MSO_FILL.GRADIENT:
|
||||
raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
|
||||
return self._fill.gradient_stops
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
"""Return member of :ref:`MsoPatternType` indicating fill pattern.
|
||||
|
||||
Raises |TypeError| when fill is not patterned (call
|
||||
`fill.patterned()` first). Returns |None| if no pattern has been set;
|
||||
PowerPoint may display the default `PERCENT_5` pattern in this case.
|
||||
Assigning |None| will remove any explicit pattern setting, although
|
||||
relying on the default behavior is discouraged and may produce
|
||||
rendering differences across client applications.
|
||||
"""
|
||||
return self._fill.pattern
|
||||
|
||||
@pattern.setter
|
||||
def pattern(self, pattern_type):
|
||||
self._fill.pattern = pattern_type
|
||||
|
||||
def patterned(self):
|
||||
"""Selects the pattern fill type.
|
||||
|
||||
Note that calling this method does not by itself set a foreground or
|
||||
background color of the pattern. Rather it enables subsequent
|
||||
assignments to properties like fore_color to set the pattern and
|
||||
colors.
|
||||
"""
|
||||
pattFill = self._xPr.get_or_change_to_pattFill()
|
||||
self._fill = _PattFill(pattFill)
|
||||
|
||||
def solid(self):
|
||||
"""
|
||||
Sets the fill type to solid, i.e. a solid color. Note that calling
|
||||
this method does not set a color or by itself cause the shape to
|
||||
appear with a solid color fill; rather it enables subsequent
|
||||
assignments to properties like fore_color to set the color.
|
||||
"""
|
||||
solidFill = self._xPr.get_or_change_to_solidFill()
|
||||
self._fill = _SolidFill(solidFill)
|
||||
|
||||
@property
|
||||
def type(self) -> MSO_FILL_TYPE:
|
||||
"""The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`."""
|
||||
return self._fill.type
|
||||
|
||||
|
||||
class _Fill(object):
|
||||
"""
|
||||
Object factory for fill object of class matching fill element, such as
|
||||
_SolidFill for ``<a:solidFill>``; also serves as the base class for all
|
||||
fill classes
|
||||
"""
|
||||
|
||||
def __new__(cls, xFill):
|
||||
if xFill is None:
|
||||
fill_cls = _NoneFill
|
||||
elif isinstance(xFill, CT_BlipFillProperties):
|
||||
fill_cls = _BlipFill
|
||||
elif isinstance(xFill, CT_GradientFillProperties):
|
||||
fill_cls = _GradFill
|
||||
elif isinstance(xFill, CT_GroupFillProperties):
|
||||
fill_cls = _GrpFill
|
||||
elif isinstance(xFill, CT_NoFillProperties):
|
||||
fill_cls = _NoFill
|
||||
elif isinstance(xFill, CT_PatternFillProperties):
|
||||
fill_cls = _PattFill
|
||||
elif isinstance(xFill, CT_SolidColorFillProperties):
|
||||
fill_cls = _SolidFill
|
||||
else:
|
||||
fill_cls = _Fill
|
||||
return super(_Fill, cls).__new__(fill_cls)
|
||||
|
||||
@property
|
||||
def back_color(self):
|
||||
"""Raise TypeError for types that do not override this property."""
|
||||
tmpl = "fill type %s has no background color, call .patterned() first"
|
||||
raise TypeError(tmpl % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def fore_color(self):
|
||||
"""Raise TypeError for types that do not override this property."""
|
||||
tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first"
|
||||
raise TypeError(tmpl % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
"""Raise TypeError for fills that do not override this property."""
|
||||
tmpl = "fill type %s has no pattern, call .patterned() first"
|
||||
raise TypeError(tmpl % self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def type(self) -> MSO_FILL_TYPE: # pragma: no cover
|
||||
raise NotImplementedError(
|
||||
f".type property must be implemented on {self.__class__.__name__}"
|
||||
)
|
||||
|
||||
|
||||
class _BlipFill(_Fill):
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.PICTURE
|
||||
|
||||
|
||||
class _GradFill(_Fill):
|
||||
"""Proxies an `a:gradFill` element."""
|
||||
|
||||
def __init__(self, gradFill):
|
||||
self._element = self._gradFill = gradFill
|
||||
|
||||
@property
|
||||
def gradient_angle(self):
|
||||
"""Angle in float degrees of line of a linear gradient.
|
||||
|
||||
Read/Write. May be |None|, indicating the angle is inherited from the
|
||||
style hierarchy. An angle of 0.0 corresponds to a left-to-right
|
||||
gradient. Increasing angles represent clockwise rotation of the line,
|
||||
for example 90.0 represents a top-to-bottom gradient. Raises
|
||||
|TypeError| when the fill type is not MSO_FILL_TYPE.GRADIENT. Raises
|
||||
|ValueError| for a non-linear gradient (e.g. a radial gradient).
|
||||
"""
|
||||
# ---case 1: gradient path is explicit, but not linear---
|
||||
path = self._gradFill.path
|
||||
if path is not None:
|
||||
raise ValueError("not a linear gradient")
|
||||
|
||||
# ---case 2: gradient path is inherited (no a:lin OR a:path)---
|
||||
lin = self._gradFill.lin
|
||||
if lin is None:
|
||||
return None
|
||||
|
||||
# ---case 3: gradient path is explicitly linear---
|
||||
# angle is stored in XML as a clockwise angle, whereas the UI
|
||||
# reports it as counter-clockwise from horizontal-pointing-right.
|
||||
# Since the UI is consistent with trigonometry conventions, we
|
||||
# respect that in the API.
|
||||
clockwise_angle = lin.ang
|
||||
counter_clockwise_angle = 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle)
|
||||
return counter_clockwise_angle
|
||||
|
||||
@gradient_angle.setter
|
||||
def gradient_angle(self, value):
|
||||
lin = self._gradFill.lin
|
||||
if lin is None:
|
||||
raise ValueError("not a linear gradient")
|
||||
lin.ang = 360.0 - value
|
||||
|
||||
@lazyproperty
|
||||
def gradient_stops(self):
|
||||
"""|_GradientStops| object providing access to gradient colors.
|
||||
|
||||
Each stop represents a color between which the gradient smoothly
|
||||
transitions.
|
||||
"""
|
||||
return _GradientStops(self._gradFill.get_or_add_gsLst())
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.GRADIENT
|
||||
|
||||
|
||||
class _GrpFill(_Fill):
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.GROUP
|
||||
|
||||
|
||||
class _NoFill(_Fill):
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.BACKGROUND
|
||||
|
||||
|
||||
class _NoneFill(_Fill):
|
||||
@property
|
||||
def type(self):
|
||||
return None
|
||||
|
||||
|
||||
class _PattFill(_Fill):
|
||||
"""Provides access to patterned fill properties."""
|
||||
|
||||
def __init__(self, pattFill):
|
||||
super(_PattFill, self).__init__()
|
||||
self._element = self._pattFill = pattFill
|
||||
|
||||
@lazyproperty
|
||||
def back_color(self):
|
||||
"""Return |ColorFormat| object that controls background color."""
|
||||
bgClr = self._pattFill.get_or_add_bgClr()
|
||||
return ColorFormat.from_colorchoice_parent(bgClr)
|
||||
|
||||
@lazyproperty
|
||||
def fore_color(self):
|
||||
"""Return |ColorFormat| object that controls foreground color."""
|
||||
fgClr = self._pattFill.get_or_add_fgClr()
|
||||
return ColorFormat.from_colorchoice_parent(fgClr)
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
"""Return member of :ref:`MsoPatternType` indicating fill pattern.
|
||||
|
||||
Returns |None| if no pattern has been set; PowerPoint may display the
|
||||
default `PERCENT_5` pattern in this case. Assigning |None| will
|
||||
remove any explicit pattern setting.
|
||||
"""
|
||||
return self._pattFill.prst
|
||||
|
||||
@pattern.setter
|
||||
def pattern(self, pattern_type):
|
||||
self._pattFill.prst = pattern_type
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.PATTERNED
|
||||
|
||||
|
||||
class _SolidFill(_Fill):
|
||||
"""Provides access to fill properties such as color for solid fills."""
|
||||
|
||||
def __init__(self, solidFill):
|
||||
super(_SolidFill, self).__init__()
|
||||
self._solidFill = solidFill
|
||||
|
||||
@lazyproperty
|
||||
def fore_color(self):
|
||||
"""Return |ColorFormat| object controlling fill color."""
|
||||
return ColorFormat.from_colorchoice_parent(self._solidFill)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return MSO_FILL.SOLID
|
||||
|
||||
|
||||
class _GradientStops(Sequence):
|
||||
"""Collection of |GradientStop| objects defining gradient colors.
|
||||
|
||||
A gradient must have a minimum of two stops, but can have as many more
|
||||
than that as required to achieve the desired effect (three is perhaps
|
||||
most common). Stops are sequenced in the order they are transitioned
|
||||
through.
|
||||
"""
|
||||
|
||||
def __init__(self, gsLst):
|
||||
self._gsLst = gsLst
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return _GradientStop(self._gsLst[idx])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._gsLst)
|
||||
|
||||
|
||||
class _GradientStop(ElementProxy):
|
||||
"""A single gradient stop.
|
||||
|
||||
A gradient stop defines a color and a position.
|
||||
"""
|
||||
|
||||
def __init__(self, gs):
|
||||
super(_GradientStop, self).__init__(gs)
|
||||
self._gs = gs
|
||||
|
||||
@lazyproperty
|
||||
def color(self):
|
||||
"""Return |ColorFormat| object controlling stop color."""
|
||||
return ColorFormat.from_colorchoice_parent(self._gs)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""Location of stop in gradient path as float between 0.0 and 1.0.
|
||||
|
||||
The value represents a percentage, where 0.0 (0%) represents the
|
||||
start of the path and 1.0 (100%) represents the end of the path. For
|
||||
a linear gradient, these would represent opposing extents of the
|
||||
filled area.
|
||||
"""
|
||||
return self._gs.pos
|
||||
|
||||
@position.setter
|
||||
def position(self, value):
|
||||
self._gs.pos = float(value)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""DrawingML objects related to line formatting."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.dml.fill import FillFormat
|
||||
from pptx.enum.dml import MSO_FILL
|
||||
from pptx.util import Emu, lazyproperty
|
||||
|
||||
|
||||
class LineFormat(object):
|
||||
"""Provides access to line properties such as color, style, and width.
|
||||
|
||||
A LineFormat object is typically accessed via the ``.line`` property of
|
||||
a shape such as |Shape| or |Picture|.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(LineFormat, self).__init__()
|
||||
self._parent = parent
|
||||
|
||||
@lazyproperty
|
||||
def color(self):
|
||||
"""
|
||||
The |ColorFormat| instance that provides access to the color settings
|
||||
for this line. Essentially a shortcut for ``line.fill.fore_color``.
|
||||
As a side-effect, accessing this property causes the line fill type
|
||||
to be set to ``MSO_FILL.SOLID``. If this sounds risky for your use
|
||||
case, use ``line.fill.type`` to non-destructively discover the
|
||||
existing fill type.
|
||||
"""
|
||||
if self.fill.type != MSO_FILL.SOLID:
|
||||
self.fill.solid()
|
||||
return self.fill.fore_color
|
||||
|
||||
@property
|
||||
def dash_style(self):
|
||||
"""Return value indicating line style.
|
||||
|
||||
Returns a member of :ref:`MsoLineDashStyle` indicating line style, or
|
||||
|None| if no explicit value has been set. When no explicit value has
|
||||
been set, the line dash style is inherited from the style hierarchy.
|
||||
|
||||
Assigning |None| removes any existing explicitly-defined dash style.
|
||||
"""
|
||||
ln = self._ln
|
||||
if ln is None:
|
||||
return None
|
||||
return ln.prstDash_val
|
||||
|
||||
@dash_style.setter
|
||||
def dash_style(self, dash_style):
|
||||
if dash_style is None:
|
||||
ln = self._ln
|
||||
if ln is None:
|
||||
return
|
||||
ln._remove_prstDash()
|
||||
ln._remove_custDash()
|
||||
return
|
||||
ln = self._get_or_add_ln()
|
||||
ln.prstDash_val = dash_style
|
||||
|
||||
@lazyproperty
|
||||
def fill(self):
|
||||
"""
|
||||
|FillFormat| instance for this line, providing access to fill
|
||||
properties such as foreground color.
|
||||
"""
|
||||
ln = self._get_or_add_ln()
|
||||
return FillFormat.from_fill_parent(ln)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
The width of the line expressed as an integer number of :ref:`English
|
||||
Metric Units <EMU>`. The returned value is an instance of |Length|,
|
||||
a value class having properties such as `.inches`, `.cm`, and `.pt`
|
||||
for converting the value into convenient units.
|
||||
"""
|
||||
ln = self._ln
|
||||
if ln is None:
|
||||
return Emu(0)
|
||||
return ln.w
|
||||
|
||||
@width.setter
|
||||
def width(self, emu):
|
||||
if emu is None:
|
||||
emu = 0
|
||||
ln = self._get_or_add_ln()
|
||||
ln.w = emu
|
||||
|
||||
def _get_or_add_ln(self):
|
||||
"""
|
||||
Return the ``<a:ln>`` element containing the line format properties
|
||||
in the XML.
|
||||
"""
|
||||
return self._parent.get_or_add_ln()
|
||||
|
||||
@property
|
||||
def _ln(self):
|
||||
return self._parent.ln
|
||||
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,71 @@
|
||||
"""Enumerations that describe click-action settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.base import BaseEnum
|
||||
|
||||
|
||||
class PP_ACTION_TYPE(BaseEnum):
|
||||
"""
|
||||
Specifies the type of a mouse action (click or hover action).
|
||||
|
||||
Alias: ``PP_ACTION``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.action import PP_ACTION
|
||||
|
||||
assert shape.click_action.action == PP_ACTION.HYPERLINK
|
||||
|
||||
MS API name: `PpActionType`
|
||||
|
||||
https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx
|
||||
"""
|
||||
|
||||
END_SHOW = (6, "Slide show ends.")
|
||||
"""Slide show ends."""
|
||||
|
||||
FIRST_SLIDE = (3, "Returns to the first slide.")
|
||||
"""Returns to the first slide."""
|
||||
|
||||
HYPERLINK = (7, "Hyperlink.")
|
||||
"""Hyperlink."""
|
||||
|
||||
LAST_SLIDE = (4, "Moves to the last slide.")
|
||||
"""Moves to the last slide."""
|
||||
|
||||
LAST_SLIDE_VIEWED = (5, "Moves to the last slide viewed.")
|
||||
"""Moves to the last slide viewed."""
|
||||
|
||||
NAMED_SLIDE = (101, "Moves to slide specified by slide number.")
|
||||
"""Moves to slide specified by slide number."""
|
||||
|
||||
NAMED_SLIDE_SHOW = (10, "Runs the slideshow.")
|
||||
"""Runs the slideshow."""
|
||||
|
||||
NEXT_SLIDE = (1, "Moves to the next slide.")
|
||||
"""Moves to the next slide."""
|
||||
|
||||
NONE = (0, "No action is performed.")
|
||||
"""No action is performed."""
|
||||
|
||||
OPEN_FILE = (102, "Opens the specified file.")
|
||||
"""Opens the specified file."""
|
||||
|
||||
OLE_VERB = (11, "OLE Verb.")
|
||||
"""OLE Verb."""
|
||||
|
||||
PLAY = (12, "Begins the slideshow.")
|
||||
"""Begins the slideshow."""
|
||||
|
||||
PREVIOUS_SLIDE = (2, "Moves to the previous slide.")
|
||||
"""Moves to the previous slide."""
|
||||
|
||||
RUN_MACRO = (8, "Runs a macro.")
|
||||
"""Runs a macro."""
|
||||
|
||||
RUN_PROGRAM = (9, "Runs a program.")
|
||||
"""Runs a program."""
|
||||
|
||||
|
||||
PP_ACTION = PP_ACTION_TYPE
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Base classes and other objects used by enumerations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Any, Type, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
_T = TypeVar("_T", bound="BaseXmlEnum")
|
||||
|
||||
|
||||
class BaseEnum(int, enum.Enum):
|
||||
"""Base class for Enums that do not map XML attr values.
|
||||
|
||||
The enum's value will be an integer, corresponding to the integer assigned the
|
||||
corresponding member in the MS API enum of the same name.
|
||||
"""
|
||||
|
||||
def __new__(cls, ms_api_value: int, docstr: str):
|
||||
self = int.__new__(cls, ms_api_value)
|
||||
self._value_ = ms_api_value
|
||||
self.__doc__ = docstr.strip()
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
"""The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
|
||||
return f"{self.name} ({self.value})"
|
||||
|
||||
|
||||
class BaseXmlEnum(int, enum.Enum):
|
||||
"""Base class for Enums that also map XML attr values.
|
||||
|
||||
The enum's value will be an integer, corresponding to the integer assigned the
|
||||
corresponding member in the MS API enum of the same name.
|
||||
"""
|
||||
|
||||
xml_value: str | None
|
||||
|
||||
def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str):
|
||||
self = int.__new__(cls, ms_api_value)
|
||||
self._value_ = ms_api_value
|
||||
self.xml_value = xml_value
|
||||
self.__doc__ = docstr.strip()
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
"""The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
|
||||
return f"{self.name} ({self.value})"
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_value: str) -> Self:
|
||||
"""Enumeration member corresponding to XML attribute value `xml_value`.
|
||||
|
||||
Raises `ValueError` if `xml_value` is the empty string ("") or is not an XML attribute
|
||||
value registered on the enumeration. Note that enum members that do not correspond to one
|
||||
of the defined values for an XML attribute have `xml_value == ""`. These
|
||||
"return-value only" members cannot be automatically mapped from an XML attribute value and
|
||||
must be selected explicitly by code, based on the appropriate conditions.
|
||||
|
||||
Example::
|
||||
|
||||
>>> WD_PARAGRAPH_ALIGNMENT.from_xml("center")
|
||||
WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
|
||||
"""
|
||||
# -- the empty string never maps to a member --
|
||||
member = (
|
||||
next((member for member in cls if member.xml_value == xml_value), None)
|
||||
if xml_value
|
||||
else None
|
||||
)
|
||||
|
||||
if member is None:
|
||||
raise ValueError(f"{cls.__name__} has no XML mapping for {repr(xml_value)}")
|
||||
|
||||
return member
|
||||
|
||||
@classmethod
|
||||
def to_xml(cls: Type[_T], value: int | _T) -> str:
|
||||
"""XML value of this enum member, generally an XML attribute value."""
|
||||
# -- presence of multi-arg `__new__()` method fools type-checker, but getting a
|
||||
# -- member by its value using EnumCls(val) works as usual.
|
||||
member = cls(value)
|
||||
xml_value = member.xml_value
|
||||
if not xml_value:
|
||||
raise ValueError(f"{cls.__name__}.{member.name} has no XML representation")
|
||||
return xml_value
|
||||
|
||||
@classmethod
|
||||
def validate(cls: Type[_T], value: _T):
|
||||
"""Raise |ValueError| if `value` is not an assignable value."""
|
||||
if value not in cls:
|
||||
raise ValueError(f"{value} not a member of {cls.__name__} enumeration")
|
||||
|
||||
|
||||
class DocsPageFormatter(object):
|
||||
"""Formats a reStructuredText documention page (string) for an enumeration."""
|
||||
|
||||
def __init__(self, clsname: str, clsdict: dict[str, Any]):
|
||||
self._clsname = clsname
|
||||
self._clsdict = clsdict
|
||||
|
||||
@property
|
||||
def page_str(self):
|
||||
"""
|
||||
The RestructuredText documentation page for the enumeration. This is
|
||||
the only API member for the class.
|
||||
"""
|
||||
tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s"
|
||||
components = (
|
||||
self._ms_name,
|
||||
self._page_title,
|
||||
self._intro_text,
|
||||
self._member_defs,
|
||||
)
|
||||
return tmpl % components
|
||||
|
||||
@property
|
||||
def _intro_text(self):
|
||||
"""
|
||||
The docstring of the enumeration, formatted for use at the top of the
|
||||
documentation page
|
||||
"""
|
||||
try:
|
||||
cls_docstring = self._clsdict["__doc__"]
|
||||
except KeyError:
|
||||
cls_docstring = ""
|
||||
|
||||
if cls_docstring is None:
|
||||
return ""
|
||||
|
||||
return textwrap.dedent(cls_docstring).strip()
|
||||
|
||||
def _member_def(self, member: BaseEnum | BaseXmlEnum):
|
||||
"""Return an individual member definition formatted as an RST glossary entry.
|
||||
|
||||
Output is wrapped to fit within 78 columns.
|
||||
"""
|
||||
member_docstring = textwrap.dedent(member.__doc__ or "").strip()
|
||||
member_docstring = textwrap.fill(
|
||||
member_docstring,
|
||||
width=78,
|
||||
initial_indent=" " * 4,
|
||||
subsequent_indent=" " * 4,
|
||||
)
|
||||
return "%s\n%s\n" % (member.name, member_docstring)
|
||||
|
||||
@property
|
||||
def _member_defs(self):
|
||||
"""
|
||||
A single string containing the aggregated member definitions section
|
||||
of the documentation page
|
||||
"""
|
||||
members = self._clsdict["__members__"]
|
||||
member_defs = [self._member_def(member) for member in members if member.name is not None]
|
||||
return "\n".join(member_defs)
|
||||
|
||||
@property
|
||||
def _ms_name(self):
|
||||
"""
|
||||
The Microsoft API name for this enumeration
|
||||
"""
|
||||
return self._clsdict["__ms_name__"]
|
||||
|
||||
@property
|
||||
def _page_title(self):
|
||||
"""
|
||||
The title for the documentation page, formatted as code (surrounded
|
||||
in double-backtics) and underlined with '=' characters
|
||||
"""
|
||||
title_underscore = "=" * (len(self._clsname) + 4)
|
||||
return "``%s``\n%s" % (self._clsname, title_underscore)
|
||||
@@ -0,0 +1,492 @@
|
||||
"""Enumerations used by charts and related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.base import BaseEnum, BaseXmlEnum
|
||||
|
||||
|
||||
class XL_AXIS_CROSSES(BaseXmlEnum):
|
||||
"""Specifies the point on an axis where the other axis crosses.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_AXIS_CROSSES
|
||||
|
||||
value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM
|
||||
|
||||
MS API Name: `XlAxisCrosses`
|
||||
|
||||
https://msdn.microsoft.com/en-us/library/office/ff745402.aspx
|
||||
"""
|
||||
|
||||
AUTOMATIC = (-4105, "autoZero", "The axis crossing point is set automatically, often at zero.")
|
||||
"""The axis crossing point is set automatically, often at zero."""
|
||||
|
||||
CUSTOM = (-4114, "", "The .crosses_at property specifies the axis crossing point.")
|
||||
"""The .crosses_at property specifies the axis crossing point."""
|
||||
|
||||
MAXIMUM = (2, "max", "The axis crosses at the maximum value.")
|
||||
"""The axis crosses at the maximum value."""
|
||||
|
||||
MINIMUM = (4, "min", "The axis crosses at the minimum value.")
|
||||
"""The axis crosses at the minimum value."""
|
||||
|
||||
|
||||
class XL_CATEGORY_TYPE(BaseEnum):
|
||||
"""Specifies the type of the category axis.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_CATEGORY_TYPE
|
||||
|
||||
date_axis = chart.category_axis
|
||||
assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE
|
||||
|
||||
MS API Name: `XlCategoryType`
|
||||
|
||||
https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx
|
||||
"""
|
||||
|
||||
AUTOMATIC_SCALE = (-4105, "The application controls the axis type.")
|
||||
"""The application controls the axis type."""
|
||||
|
||||
CATEGORY_SCALE = (2, "Axis groups data by an arbitrary set of categories")
|
||||
"""Axis groups data by an arbitrary set of categories"""
|
||||
|
||||
TIME_SCALE = (3, "Axis groups data on a time scale of days, months, or years.")
|
||||
"""Axis groups data on a time scale of days, months, or years."""
|
||||
|
||||
|
||||
class XL_CHART_TYPE(BaseEnum):
|
||||
"""Specifies the type of a chart.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_CHART_TYPE
|
||||
|
||||
assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED
|
||||
|
||||
MS API Name: `XlChartType`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff838409.aspx
|
||||
"""
|
||||
|
||||
THREE_D_AREA = (-4098, "3D Area.")
|
||||
"""3D Area."""
|
||||
|
||||
THREE_D_AREA_STACKED = (78, "3D Stacked Area.")
|
||||
"""3D Stacked Area."""
|
||||
|
||||
THREE_D_AREA_STACKED_100 = (79, "100% Stacked Area.")
|
||||
"""100% Stacked Area."""
|
||||
|
||||
THREE_D_BAR_CLUSTERED = (60, "3D Clustered Bar.")
|
||||
"""3D Clustered Bar."""
|
||||
|
||||
THREE_D_BAR_STACKED = (61, "3D Stacked Bar.")
|
||||
"""3D Stacked Bar."""
|
||||
|
||||
THREE_D_BAR_STACKED_100 = (62, "3D 100% Stacked Bar.")
|
||||
"""3D 100% Stacked Bar."""
|
||||
|
||||
THREE_D_COLUMN = (-4100, "3D Column.")
|
||||
"""3D Column."""
|
||||
|
||||
THREE_D_COLUMN_CLUSTERED = (54, "3D Clustered Column.")
|
||||
"""3D Clustered Column."""
|
||||
|
||||
THREE_D_COLUMN_STACKED = (55, "3D Stacked Column.")
|
||||
"""3D Stacked Column."""
|
||||
|
||||
THREE_D_COLUMN_STACKED_100 = (56, "3D 100% Stacked Column.")
|
||||
"""3D 100% Stacked Column."""
|
||||
|
||||
THREE_D_LINE = (-4101, "3D Line.")
|
||||
"""3D Line."""
|
||||
|
||||
THREE_D_PIE = (-4102, "3D Pie.")
|
||||
"""3D Pie."""
|
||||
|
||||
THREE_D_PIE_EXPLODED = (70, "Exploded 3D Pie.")
|
||||
"""Exploded 3D Pie."""
|
||||
|
||||
AREA = (1, "Area")
|
||||
"""Area"""
|
||||
|
||||
AREA_STACKED = (76, "Stacked Area.")
|
||||
"""Stacked Area."""
|
||||
|
||||
AREA_STACKED_100 = (77, "100% Stacked Area.")
|
||||
"""100% Stacked Area."""
|
||||
|
||||
BAR_CLUSTERED = (57, "Clustered Bar.")
|
||||
"""Clustered Bar."""
|
||||
|
||||
BAR_OF_PIE = (71, "Bar of Pie.")
|
||||
"""Bar of Pie."""
|
||||
|
||||
BAR_STACKED = (58, "Stacked Bar.")
|
||||
"""Stacked Bar."""
|
||||
|
||||
BAR_STACKED_100 = (59, "100% Stacked Bar.")
|
||||
"""100% Stacked Bar."""
|
||||
|
||||
BUBBLE = (15, "Bubble.")
|
||||
"""Bubble."""
|
||||
|
||||
BUBBLE_THREE_D_EFFECT = (87, "Bubble with 3D effects.")
|
||||
"""Bubble with 3D effects."""
|
||||
|
||||
COLUMN_CLUSTERED = (51, "Clustered Column.")
|
||||
"""Clustered Column."""
|
||||
|
||||
COLUMN_STACKED = (52, "Stacked Column.")
|
||||
"""Stacked Column."""
|
||||
|
||||
COLUMN_STACKED_100 = (53, "100% Stacked Column.")
|
||||
"""100% Stacked Column."""
|
||||
|
||||
CONE_BAR_CLUSTERED = (102, "Clustered Cone Bar.")
|
||||
"""Clustered Cone Bar."""
|
||||
|
||||
CONE_BAR_STACKED = (103, "Stacked Cone Bar.")
|
||||
"""Stacked Cone Bar."""
|
||||
|
||||
CONE_BAR_STACKED_100 = (104, "100% Stacked Cone Bar.")
|
||||
"""100% Stacked Cone Bar."""
|
||||
|
||||
CONE_COL = (105, "3D Cone Column.")
|
||||
"""3D Cone Column."""
|
||||
|
||||
CONE_COL_CLUSTERED = (99, "Clustered Cone Column.")
|
||||
"""Clustered Cone Column."""
|
||||
|
||||
CONE_COL_STACKED = (100, "Stacked Cone Column.")
|
||||
"""Stacked Cone Column."""
|
||||
|
||||
CONE_COL_STACKED_100 = (101, "100% Stacked Cone Column.")
|
||||
"""100% Stacked Cone Column."""
|
||||
|
||||
CYLINDER_BAR_CLUSTERED = (95, "Clustered Cylinder Bar.")
|
||||
"""Clustered Cylinder Bar."""
|
||||
|
||||
CYLINDER_BAR_STACKED = (96, "Stacked Cylinder Bar.")
|
||||
"""Stacked Cylinder Bar."""
|
||||
|
||||
CYLINDER_BAR_STACKED_100 = (97, "100% Stacked Cylinder Bar.")
|
||||
"""100% Stacked Cylinder Bar."""
|
||||
|
||||
CYLINDER_COL = (98, "3D Cylinder Column.")
|
||||
"""3D Cylinder Column."""
|
||||
|
||||
CYLINDER_COL_CLUSTERED = (92, "Clustered Cone Column.")
|
||||
"""Clustered Cone Column."""
|
||||
|
||||
CYLINDER_COL_STACKED = (93, "Stacked Cone Column.")
|
||||
"""Stacked Cone Column."""
|
||||
|
||||
CYLINDER_COL_STACKED_100 = (94, "100% Stacked Cylinder Column.")
|
||||
"""100% Stacked Cylinder Column."""
|
||||
|
||||
DOUGHNUT = (-4120, "Doughnut.")
|
||||
"""Doughnut."""
|
||||
|
||||
DOUGHNUT_EXPLODED = (80, "Exploded Doughnut.")
|
||||
"""Exploded Doughnut."""
|
||||
|
||||
LINE = (4, "Line.")
|
||||
"""Line."""
|
||||
|
||||
LINE_MARKERS = (65, "Line with Markers.")
|
||||
"""Line with Markers."""
|
||||
|
||||
LINE_MARKERS_STACKED = (66, "Stacked Line with Markers.")
|
||||
"""Stacked Line with Markers."""
|
||||
|
||||
LINE_MARKERS_STACKED_100 = (67, "100% Stacked Line with Markers.")
|
||||
"""100% Stacked Line with Markers."""
|
||||
|
||||
LINE_STACKED = (63, "Stacked Line.")
|
||||
"""Stacked Line."""
|
||||
|
||||
LINE_STACKED_100 = (64, "100% Stacked Line.")
|
||||
"""100% Stacked Line."""
|
||||
|
||||
PIE = (5, "Pie.")
|
||||
"""Pie."""
|
||||
|
||||
PIE_EXPLODED = (69, "Exploded Pie.")
|
||||
"""Exploded Pie."""
|
||||
|
||||
PIE_OF_PIE = (68, "Pie of Pie.")
|
||||
"""Pie of Pie."""
|
||||
|
||||
PYRAMID_BAR_CLUSTERED = (109, "Clustered Pyramid Bar.")
|
||||
"""Clustered Pyramid Bar."""
|
||||
|
||||
PYRAMID_BAR_STACKED = (110, "Stacked Pyramid Bar.")
|
||||
"""Stacked Pyramid Bar."""
|
||||
|
||||
PYRAMID_BAR_STACKED_100 = (111, "100% Stacked Pyramid Bar.")
|
||||
"""100% Stacked Pyramid Bar."""
|
||||
|
||||
PYRAMID_COL = (112, "3D Pyramid Column.")
|
||||
"""3D Pyramid Column."""
|
||||
|
||||
PYRAMID_COL_CLUSTERED = (106, "Clustered Pyramid Column.")
|
||||
"""Clustered Pyramid Column."""
|
||||
|
||||
PYRAMID_COL_STACKED = (107, "Stacked Pyramid Column.")
|
||||
"""Stacked Pyramid Column."""
|
||||
|
||||
PYRAMID_COL_STACKED_100 = (108, "100% Stacked Pyramid Column.")
|
||||
"""100% Stacked Pyramid Column."""
|
||||
|
||||
RADAR = (-4151, "Radar.")
|
||||
"""Radar."""
|
||||
|
||||
RADAR_FILLED = (82, "Filled Radar.")
|
||||
"""Filled Radar."""
|
||||
|
||||
RADAR_MARKERS = (81, "Radar with Data Markers.")
|
||||
"""Radar with Data Markers."""
|
||||
|
||||
STOCK_HLC = (88, "High-Low-Close.")
|
||||
"""High-Low-Close."""
|
||||
|
||||
STOCK_OHLC = (89, "Open-High-Low-Close.")
|
||||
"""Open-High-Low-Close."""
|
||||
|
||||
STOCK_VHLC = (90, "Volume-High-Low-Close.")
|
||||
"""Volume-High-Low-Close."""
|
||||
|
||||
STOCK_VOHLC = (91, "Volume-Open-High-Low-Close.")
|
||||
"""Volume-Open-High-Low-Close."""
|
||||
|
||||
SURFACE = (83, "3D Surface.")
|
||||
"""3D Surface."""
|
||||
|
||||
SURFACE_TOP_VIEW = (85, "Surface (Top View).")
|
||||
"""Surface (Top View)."""
|
||||
|
||||
SURFACE_TOP_VIEW_WIREFRAME = (86, "Surface (Top View wireframe).")
|
||||
"""Surface (Top View wireframe)."""
|
||||
|
||||
SURFACE_WIREFRAME = (84, "3D Surface (wireframe).")
|
||||
"""3D Surface (wireframe)."""
|
||||
|
||||
XY_SCATTER = (-4169, "Scatter.")
|
||||
"""Scatter."""
|
||||
|
||||
XY_SCATTER_LINES = (74, "Scatter with Lines.")
|
||||
"""Scatter with Lines."""
|
||||
|
||||
XY_SCATTER_LINES_NO_MARKERS = (75, "Scatter with Lines and No Data Markers.")
|
||||
"""Scatter with Lines and No Data Markers."""
|
||||
|
||||
XY_SCATTER_SMOOTH = (72, "Scatter with Smoothed Lines.")
|
||||
"""Scatter with Smoothed Lines."""
|
||||
|
||||
XY_SCATTER_SMOOTH_NO_MARKERS = (73, "Scatter with Smoothed Lines and No Data Markers.")
|
||||
"""Scatter with Smoothed Lines and No Data Markers."""
|
||||
|
||||
|
||||
class XL_DATA_LABEL_POSITION(BaseXmlEnum):
|
||||
"""Specifies where the data label is positioned.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_LABEL_POSITION
|
||||
|
||||
data_labels = chart.plots[0].data_labels
|
||||
data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
|
||||
|
||||
MS API Name: `XlDataLabelPosition`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff745082.aspx
|
||||
"""
|
||||
|
||||
ABOVE = (0, "t", "The data label is positioned above the data point.")
|
||||
"""The data label is positioned above the data point."""
|
||||
|
||||
BELOW = (1, "b", "The data label is positioned below the data point.")
|
||||
"""The data label is positioned below the data point."""
|
||||
|
||||
BEST_FIT = (5, "bestFit", "Word sets the position of the data label.")
|
||||
"""Word sets the position of the data label."""
|
||||
|
||||
CENTER = (
|
||||
-4108,
|
||||
"ctr",
|
||||
"The data label is centered on the data point or inside a bar or a pie slice.",
|
||||
)
|
||||
"""The data label is centered on the data point or inside a bar or a pie slice."""
|
||||
|
||||
INSIDE_BASE = (
|
||||
4,
|
||||
"inBase",
|
||||
"The data label is positioned inside the data point at the bottom edge.",
|
||||
)
|
||||
"""The data label is positioned inside the data point at the bottom edge."""
|
||||
|
||||
INSIDE_END = (3, "inEnd", "The data label is positioned inside the data point at the top edge.")
|
||||
"""The data label is positioned inside the data point at the top edge."""
|
||||
|
||||
LEFT = (-4131, "l", "The data label is positioned to the left of the data point.")
|
||||
"""The data label is positioned to the left of the data point."""
|
||||
|
||||
MIXED = (6, "", "Data labels are in multiple positions (read-only).")
|
||||
"""Data labels are in multiple positions (read-only)."""
|
||||
|
||||
OUTSIDE_END = (
|
||||
2,
|
||||
"outEnd",
|
||||
"The data label is positioned outside the data point at the top edge.",
|
||||
)
|
||||
"""The data label is positioned outside the data point at the top edge."""
|
||||
|
||||
RIGHT = (-4152, "r", "The data label is positioned to the right of the data point.")
|
||||
"""The data label is positioned to the right of the data point."""
|
||||
|
||||
|
||||
XL_LABEL_POSITION = XL_DATA_LABEL_POSITION
|
||||
|
||||
|
||||
class XL_LEGEND_POSITION(BaseXmlEnum):
|
||||
"""Specifies the position of the legend on a chart.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_LEGEND_POSITION
|
||||
|
||||
chart.has_legend = True
|
||||
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
|
||||
|
||||
MS API Name: `XlLegendPosition`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff745840.aspx
|
||||
"""
|
||||
|
||||
BOTTOM = (-4107, "b", "Below the chart.")
|
||||
"""Below the chart."""
|
||||
|
||||
CORNER = (2, "tr", "In the upper-right corner of the chart border.")
|
||||
"""In the upper-right corner of the chart border."""
|
||||
|
||||
CUSTOM = (-4161, "", "A custom position (read-only).")
|
||||
"""A custom position (read-only)."""
|
||||
|
||||
LEFT = (-4131, "l", "Left of the chart.")
|
||||
"""Left of the chart."""
|
||||
|
||||
RIGHT = (-4152, "r", "Right of the chart.")
|
||||
"""Right of the chart."""
|
||||
|
||||
TOP = (-4160, "t", "Above the chart.")
|
||||
"""Above the chart."""
|
||||
|
||||
|
||||
class XL_MARKER_STYLE(BaseXmlEnum):
|
||||
"""Specifies the marker style for a point or series in a line, scatter, or radar chart.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_MARKER_STYLE
|
||||
|
||||
series.marker.style = XL_MARKER_STYLE.CIRCLE
|
||||
|
||||
MS API Name: `XlMarkerStyle`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff197219.aspx
|
||||
"""
|
||||
|
||||
AUTOMATIC = (-4105, "auto", "Automatic markers")
|
||||
"""Automatic markers"""
|
||||
|
||||
CIRCLE = (8, "circle", "Circular markers")
|
||||
"""Circular markers"""
|
||||
|
||||
DASH = (-4115, "dash", "Long bar markers")
|
||||
"""Long bar markers"""
|
||||
|
||||
DIAMOND = (2, "diamond", "Diamond-shaped markers")
|
||||
"""Diamond-shaped markers"""
|
||||
|
||||
DOT = (-4118, "dot", "Short bar markers")
|
||||
"""Short bar markers"""
|
||||
|
||||
NONE = (-4142, "none", "No markers")
|
||||
"""No markers"""
|
||||
|
||||
PICTURE = (-4147, "picture", "Picture markers")
|
||||
"""Picture markers"""
|
||||
|
||||
PLUS = (9, "plus", "Square markers with a plus sign")
|
||||
"""Square markers with a plus sign"""
|
||||
|
||||
SQUARE = (1, "square", "Square markers")
|
||||
"""Square markers"""
|
||||
|
||||
STAR = (5, "star", "Square markers with an asterisk")
|
||||
"""Square markers with an asterisk"""
|
||||
|
||||
TRIANGLE = (3, "triangle", "Triangular markers")
|
||||
"""Triangular markers"""
|
||||
|
||||
X = (-4168, "x", "Square markers with an X")
|
||||
"""Square markers with an X"""
|
||||
|
||||
|
||||
class XL_TICK_MARK(BaseXmlEnum):
|
||||
"""Specifies a type of axis tick for a chart.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_TICK_MARK
|
||||
|
||||
chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE
|
||||
|
||||
MS API Name: `XlTickMark`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff193878.aspx
|
||||
"""
|
||||
|
||||
CROSS = (4, "cross", "Tick mark crosses the axis")
|
||||
"""Tick mark crosses the axis"""
|
||||
|
||||
INSIDE = (2, "in", "Tick mark appears inside the axis")
|
||||
"""Tick mark appears inside the axis"""
|
||||
|
||||
NONE = (-4142, "none", "No tick mark")
|
||||
"""No tick mark"""
|
||||
|
||||
OUTSIDE = (3, "out", "Tick mark appears outside the axis")
|
||||
"""Tick mark appears outside the axis"""
|
||||
|
||||
|
||||
class XL_TICK_LABEL_POSITION(BaseXmlEnum):
|
||||
"""Specifies the position of tick-mark labels on a chart axis.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.chart import XL_TICK_LABEL_POSITION
|
||||
|
||||
category_axis = chart.category_axis
|
||||
category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW
|
||||
|
||||
MS API Name: `XlTickLabelPosition`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff822561.aspx
|
||||
"""
|
||||
|
||||
HIGH = (-4127, "high", "Top or right side of the chart.")
|
||||
"""Top or right side of the chart."""
|
||||
|
||||
LOW = (-4134, "low", "Bottom or left side of the chart.")
|
||||
"""Bottom or left side of the chart."""
|
||||
|
||||
NEXT_TO_AXIS = (4, "nextTo", "Next to axis (where axis is not at either side of the chart).")
|
||||
"""Next to axis (where axis is not at either side of the chart)."""
|
||||
|
||||
NONE = (-4142, "none", "No tick labels.")
|
||||
"""No tick labels."""
|
||||
@@ -0,0 +1,405 @@
|
||||
"""Enumerations used by DrawingML objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.base import BaseEnum, BaseXmlEnum
|
||||
|
||||
|
||||
class MSO_COLOR_TYPE(BaseEnum):
|
||||
"""
|
||||
Specifies the color specification scheme
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.dml import MSO_COLOR_TYPE
|
||||
|
||||
assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME
|
||||
|
||||
MS API Name: "MsoColorType"
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx
|
||||
"""
|
||||
|
||||
RGB = (1, "Color is specified by an |RGBColor| value.")
|
||||
"""Color is specified by an |RGBColor| value."""
|
||||
|
||||
SCHEME = (2, "Color is one of the preset theme colors")
|
||||
"""Color is one of the preset theme colors"""
|
||||
|
||||
HSL = (101, "Color is specified using Hue, Saturation, and Luminosity values")
|
||||
"""Color is specified using Hue, Saturation, and Luminosity values"""
|
||||
|
||||
PRESET = (102, "Color is specified using a named built-in color")
|
||||
"""Color is specified using a named built-in color"""
|
||||
|
||||
SCRGB = (103, "Color is an scRGB color, a wide color gamut RGB color space")
|
||||
"""Color is an scRGB color, a wide color gamut RGB color space"""
|
||||
|
||||
SYSTEM = (
|
||||
104,
|
||||
"Color is one specified by the operating system, such as the window background color.",
|
||||
)
|
||||
"""Color is one specified by the operating system, such as the window background color."""
|
||||
|
||||
|
||||
class MSO_FILL_TYPE(BaseEnum):
|
||||
"""
|
||||
Specifies the type of bitmap used for the fill of a shape.
|
||||
|
||||
Alias: ``MSO_FILL``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.dml import MSO_FILL
|
||||
|
||||
assert shape.fill.type == MSO_FILL.SOLID
|
||||
|
||||
MS API Name: `MsoFillType`
|
||||
|
||||
http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx
|
||||
"""
|
||||
|
||||
BACKGROUND = (
|
||||
5,
|
||||
"The shape is transparent, such that whatever is behind the shape shows through."
|
||||
" Often this is the slide background, but if a visible shape is behind, that will"
|
||||
" show through.",
|
||||
)
|
||||
"""The shape is transparent, such that whatever is behind the shape shows through.
|
||||
|
||||
Often this is the slide background, but if a visible shape is behind, that will show through.
|
||||
"""
|
||||
|
||||
GRADIENT = (3, "Shape is filled with a gradient")
|
||||
"""Shape is filled with a gradient"""
|
||||
|
||||
GROUP = (101, "Shape is part of a group and should inherit the fill properties of the group.")
|
||||
"""Shape is part of a group and should inherit the fill properties of the group."""
|
||||
|
||||
PATTERNED = (2, "Shape is filled with a pattern")
|
||||
"""Shape is filled with a pattern"""
|
||||
|
||||
PICTURE = (6, "Shape is filled with a bitmapped image")
|
||||
"""Shape is filled with a bitmapped image"""
|
||||
|
||||
SOLID = (1, "Shape is filled with a solid color")
|
||||
"""Shape is filled with a solid color"""
|
||||
|
||||
TEXTURED = (4, "Shape is filled with a texture")
|
||||
"""Shape is filled with a texture"""
|
||||
|
||||
|
||||
MSO_FILL = MSO_FILL_TYPE
|
||||
|
||||
|
||||
class MSO_LINE_DASH_STYLE(BaseXmlEnum):
|
||||
"""Specifies the dash style for a line.
|
||||
|
||||
Alias: ``MSO_LINE``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.dml import MSO_LINE
|
||||
|
||||
shape.line.dash_style = MSO_LINE.DASH_DOT_DOT
|
||||
|
||||
MS API name: `MsoLineDashStyle`
|
||||
|
||||
https://learn.microsoft.com/en-us/office/vba/api/Office.MsoLineDashStyle
|
||||
"""
|
||||
|
||||
DASH = (4, "dash", "Line consists of dashes only.")
|
||||
"""Line consists of dashes only."""
|
||||
|
||||
DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.")
|
||||
"""Line is a dash-dot pattern."""
|
||||
|
||||
DASH_DOT_DOT = (6, "lgDashDotDot", "Line is a dash-dot-dot pattern.")
|
||||
"""Line is a dash-dot-dot pattern."""
|
||||
|
||||
LONG_DASH = (7, "lgDash", "Line consists of long dashes.")
|
||||
"""Line consists of long dashes."""
|
||||
|
||||
LONG_DASH_DOT = (8, "lgDashDot", "Line is a long dash-dot pattern.")
|
||||
"""Line is a long dash-dot pattern."""
|
||||
|
||||
ROUND_DOT = (3, "sysDot", "Line is made up of round dots.")
|
||||
"""Line is made up of round dots."""
|
||||
|
||||
SOLID = (1, "solid", "Line is solid.")
|
||||
"""Line is solid."""
|
||||
|
||||
SQUARE_DOT = (2, "sysDash", "Line is made up of square dots.")
|
||||
"""Line is made up of square dots."""
|
||||
|
||||
DASH_STYLE_MIXED = (-2, "", "Not supported.")
|
||||
"""Return value only, indicating more than one dash style applies."""
|
||||
|
||||
|
||||
MSO_LINE = MSO_LINE_DASH_STYLE
|
||||
|
||||
|
||||
class MSO_PATTERN_TYPE(BaseXmlEnum):
|
||||
"""Specifies the fill pattern used in a shape.
|
||||
|
||||
Alias: ``MSO_PATTERN``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.dml import MSO_PATTERN
|
||||
|
||||
fill = shape.fill
|
||||
fill.patterned()
|
||||
fill.pattern = MSO_PATTERN.WAVE
|
||||
|
||||
MS API Name: `MsoPatternType`
|
||||
|
||||
https://learn.microsoft.com/en-us/office/vba/api/Office.MsoPatternType
|
||||
"""
|
||||
|
||||
CROSS = (51, "cross", "Cross")
|
||||
"""Cross"""
|
||||
|
||||
DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal")
|
||||
"""Dark Downward Diagonal"""
|
||||
|
||||
DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal")
|
||||
"""Dark Horizontal"""
|
||||
|
||||
DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal")
|
||||
"""Dark Upward Diagonal"""
|
||||
|
||||
DARK_VERTICAL = (14, "dkVert", "Dark Vertical")
|
||||
"""Dark Vertical"""
|
||||
|
||||
DASHED_DOWNWARD_DIAGONAL = (28, "dashDnDiag", "Dashed Downward Diagonal")
|
||||
"""Dashed Downward Diagonal"""
|
||||
|
||||
DASHED_HORIZONTAL = (32, "dashHorz", "Dashed Horizontal")
|
||||
"""Dashed Horizontal"""
|
||||
|
||||
DASHED_UPWARD_DIAGONAL = (27, "dashUpDiag", "Dashed Upward Diagonal")
|
||||
"""Dashed Upward Diagonal"""
|
||||
|
||||
DASHED_VERTICAL = (31, "dashVert", "Dashed Vertical")
|
||||
"""Dashed Vertical"""
|
||||
|
||||
DIAGONAL_BRICK = (40, "diagBrick", "Diagonal Brick")
|
||||
"""Diagonal Brick"""
|
||||
|
||||
DIAGONAL_CROSS = (54, "diagCross", "Diagonal Cross")
|
||||
"""Diagonal Cross"""
|
||||
|
||||
DIVOT = (46, "divot", "Pattern Divot")
|
||||
"""Pattern Divot"""
|
||||
|
||||
DOTTED_DIAMOND = (24, "dotDmnd", "Dotted Diamond")
|
||||
"""Dotted Diamond"""
|
||||
|
||||
DOTTED_GRID = (45, "dotGrid", "Dotted Grid")
|
||||
"""Dotted Grid"""
|
||||
|
||||
DOWNWARD_DIAGONAL = (52, "dnDiag", "Downward Diagonal")
|
||||
"""Downward Diagonal"""
|
||||
|
||||
HORIZONTAL = (49, "horz", "Horizontal")
|
||||
"""Horizontal"""
|
||||
|
||||
HORIZONTAL_BRICK = (35, "horzBrick", "Horizontal Brick")
|
||||
"""Horizontal Brick"""
|
||||
|
||||
LARGE_CHECKER_BOARD = (36, "lgCheck", "Large Checker Board")
|
||||
"""Large Checker Board"""
|
||||
|
||||
LARGE_CONFETTI = (33, "lgConfetti", "Large Confetti")
|
||||
"""Large Confetti"""
|
||||
|
||||
LARGE_GRID = (34, "lgGrid", "Large Grid")
|
||||
"""Large Grid"""
|
||||
|
||||
LIGHT_DOWNWARD_DIAGONAL = (21, "ltDnDiag", "Light Downward Diagonal")
|
||||
"""Light Downward Diagonal"""
|
||||
|
||||
LIGHT_HORIZONTAL = (19, "ltHorz", "Light Horizontal")
|
||||
"""Light Horizontal"""
|
||||
|
||||
LIGHT_UPWARD_DIAGONAL = (22, "ltUpDiag", "Light Upward Diagonal")
|
||||
"""Light Upward Diagonal"""
|
||||
|
||||
LIGHT_VERTICAL = (20, "ltVert", "Light Vertical")
|
||||
"""Light Vertical"""
|
||||
|
||||
NARROW_HORIZONTAL = (30, "narHorz", "Narrow Horizontal")
|
||||
"""Narrow Horizontal"""
|
||||
|
||||
NARROW_VERTICAL = (29, "narVert", "Narrow Vertical")
|
||||
"""Narrow Vertical"""
|
||||
|
||||
OUTLINED_DIAMOND = (41, "openDmnd", "Outlined Diamond")
|
||||
"""Outlined Diamond"""
|
||||
|
||||
PERCENT_10 = (2, "pct10", "10% of the foreground color.")
|
||||
"""10% of the foreground color."""
|
||||
|
||||
PERCENT_20 = (3, "pct20", "20% of the foreground color.")
|
||||
"""20% of the foreground color."""
|
||||
|
||||
PERCENT_25 = (4, "pct25", "25% of the foreground color.")
|
||||
"""25% of the foreground color."""
|
||||
|
||||
PERCENT_30 = (5, "pct30", "30% of the foreground color.")
|
||||
"""30% of the foreground color."""
|
||||
|
||||
ERCENT_40 = (6, "pct40", "40% of the foreground color.")
|
||||
"""40% of the foreground color."""
|
||||
|
||||
PERCENT_5 = (1, "pct5", "5% of the foreground color.")
|
||||
"""5% of the foreground color."""
|
||||
|
||||
PERCENT_50 = (7, "pct50", "50% of the foreground color.")
|
||||
"""50% of the foreground color."""
|
||||
|
||||
PERCENT_60 = (8, "pct60", "60% of the foreground color.")
|
||||
"""60% of the foreground color."""
|
||||
|
||||
PERCENT_70 = (9, "pct70", "70% of the foreground color.")
|
||||
"""70% of the foreground color."""
|
||||
|
||||
PERCENT_75 = (10, "pct75", "75% of the foreground color.")
|
||||
"""75% of the foreground color."""
|
||||
|
||||
PERCENT_80 = (11, "pct80", "80% of the foreground color.")
|
||||
"""80% of the foreground color."""
|
||||
|
||||
PERCENT_90 = (12, "pct90", "90% of the foreground color.")
|
||||
"""90% of the foreground color."""
|
||||
|
||||
PLAID = (42, "plaid", "Plaid")
|
||||
"""Plaid"""
|
||||
|
||||
SHINGLE = (47, "shingle", "Shingle")
|
||||
"""Shingle"""
|
||||
|
||||
SMALL_CHECKER_BOARD = (17, "smCheck", "Small Checker Board")
|
||||
"""Small Checker Board"""
|
||||
|
||||
SMALL_CONFETTI = (37, "smConfetti", "Small Confetti")
|
||||
"""Small Confetti"""
|
||||
|
||||
SMALL_GRID = (23, "smGrid", "Small Grid")
|
||||
"""Small Grid"""
|
||||
|
||||
SOLID_DIAMOND = (39, "solidDmnd", "Solid Diamond")
|
||||
"""Solid Diamond"""
|
||||
|
||||
SPHERE = (43, "sphere", "Sphere")
|
||||
"""Sphere"""
|
||||
|
||||
TRELLIS = (18, "trellis", "Trellis")
|
||||
"""Trellis"""
|
||||
|
||||
UPWARD_DIAGONAL = (53, "upDiag", "Upward Diagonal")
|
||||
"""Upward Diagonal"""
|
||||
|
||||
VERTICAL = (50, "vert", "Vertical")
|
||||
"""Vertical"""
|
||||
|
||||
WAVE = (48, "wave", "Wave")
|
||||
"""Wave"""
|
||||
|
||||
WEAVE = (44, "weave", "Weave")
|
||||
"""Weave"""
|
||||
|
||||
WIDE_DOWNWARD_DIAGONAL = (25, "wdDnDiag", "Wide Downward Diagonal")
|
||||
"""Wide Downward Diagonal"""
|
||||
|
||||
WIDE_UPWARD_DIAGONAL = (26, "wdUpDiag", "Wide Upward Diagonal")
|
||||
"""Wide Upward Diagonal"""
|
||||
|
||||
ZIG_ZAG = (38, "zigZag", "Zig Zag")
|
||||
"""Zig Zag"""
|
||||
|
||||
MIXED = (-2, "", "Mixed pattern (read-only).")
|
||||
"""Mixed pattern (read-only)."""
|
||||
|
||||
|
||||
MSO_PATTERN = MSO_PATTERN_TYPE
|
||||
|
||||
|
||||
class MSO_THEME_COLOR_INDEX(BaseXmlEnum):
|
||||
"""An Office theme color, one of those shown in the color gallery on the formatting ribbon.
|
||||
|
||||
Alias: ``MSO_THEME_COLOR``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.dml import MSO_THEME_COLOR
|
||||
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1
|
||||
|
||||
MS API Name: `MsoThemeColorIndex`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx
|
||||
"""
|
||||
|
||||
NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.")
|
||||
"""Indicates the color is not a theme color."""
|
||||
|
||||
ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.")
|
||||
"""Specifies the Accent 1 theme color."""
|
||||
|
||||
ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.")
|
||||
"""Specifies the Accent 2 theme color."""
|
||||
|
||||
ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.")
|
||||
"""Specifies the Accent 3 theme color."""
|
||||
|
||||
ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.")
|
||||
"""Specifies the Accent 4 theme color."""
|
||||
|
||||
ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.")
|
||||
"""Specifies the Accent 5 theme color."""
|
||||
|
||||
ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.")
|
||||
"""Specifies the Accent 6 theme color."""
|
||||
|
||||
BACKGROUND_1 = (14, "bg1", "Specifies the Background 1 theme color.")
|
||||
"""Specifies the Background 1 theme color."""
|
||||
|
||||
BACKGROUND_2 = (16, "bg2", "Specifies the Background 2 theme color.")
|
||||
"""Specifies the Background 2 theme color."""
|
||||
|
||||
DARK_1 = (1, "dk1", "Specifies the Dark 1 theme color.")
|
||||
"""Specifies the Dark 1 theme color."""
|
||||
|
||||
DARK_2 = (3, "dk2", "Specifies the Dark 2 theme color.")
|
||||
"""Specifies the Dark 2 theme color."""
|
||||
|
||||
FOLLOWED_HYPERLINK = (12, "folHlink", "Specifies the theme color for a clicked hyperlink.")
|
||||
"""Specifies the theme color for a clicked hyperlink."""
|
||||
|
||||
HYPERLINK = (11, "hlink", "Specifies the theme color for a hyperlink.")
|
||||
"""Specifies the theme color for a hyperlink."""
|
||||
|
||||
LIGHT_1 = (2, "lt1", "Specifies the Light 1 theme color.")
|
||||
"""Specifies the Light 1 theme color."""
|
||||
|
||||
LIGHT_2 = (4, "lt2", "Specifies the Light 2 theme color.")
|
||||
"""Specifies the Light 2 theme color."""
|
||||
|
||||
TEXT_1 = (13, "tx1", "Specifies the Text 1 theme color.")
|
||||
"""Specifies the Text 1 theme color."""
|
||||
|
||||
TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.")
|
||||
"""Specifies the Text 2 theme color."""
|
||||
|
||||
MIXED = (
|
||||
-2,
|
||||
"",
|
||||
"Indicates multiple theme colors are used, such as in a group shape (read-only).",
|
||||
)
|
||||
"""Indicates multiple theme colors are used, such as in a group shape (read-only)."""
|
||||
|
||||
|
||||
MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX
|
||||
@@ -0,0 +1,685 @@
|
||||
"""Enumerations used for specifying language."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.base import BaseXmlEnum
|
||||
|
||||
|
||||
class MSO_LANGUAGE_ID(BaseXmlEnum):
|
||||
"""
|
||||
Specifies the language identifier.
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.lang import MSO_LANGUAGE_ID
|
||||
|
||||
font.language_id = MSO_LANGUAGE_ID.POLISH
|
||||
|
||||
MS API Name: `MsoLanguageId`
|
||||
|
||||
https://msdn.microsoft.com/en-us/library/office/ff862134.aspx
|
||||
"""
|
||||
|
||||
NONE = (0, "", "No language specified.")
|
||||
"""No language specified."""
|
||||
|
||||
AFRIKAANS = (1078, "af-ZA", "The Afrikaans language.")
|
||||
"""The Afrikaans language."""
|
||||
|
||||
ALBANIAN = (1052, "sq-AL", "The Albanian language.")
|
||||
"""The Albanian language."""
|
||||
|
||||
AMHARIC = (1118, "am-ET", "The Amharic language.")
|
||||
"""The Amharic language."""
|
||||
|
||||
ARABIC = (1025, "ar-SA", "The Arabic language.")
|
||||
"""The Arabic language."""
|
||||
|
||||
ARABIC_ALGERIA = (5121, "ar-DZ", "The Arabic Algeria language.")
|
||||
"""The Arabic Algeria language."""
|
||||
|
||||
ARABIC_BAHRAIN = (15361, "ar-BH", "The Arabic Bahrain language.")
|
||||
"""The Arabic Bahrain language."""
|
||||
|
||||
ARABIC_EGYPT = (3073, "ar-EG", "The Arabic Egypt language.")
|
||||
"""The Arabic Egypt language."""
|
||||
|
||||
ARABIC_IRAQ = (2049, "ar-IQ", "The Arabic Iraq language.")
|
||||
"""The Arabic Iraq language."""
|
||||
|
||||
ARABIC_JORDAN = (11265, "ar-JO", "The Arabic Jordan language.")
|
||||
"""The Arabic Jordan language."""
|
||||
|
||||
ARABIC_KUWAIT = (13313, "ar-KW", "The Arabic Kuwait language.")
|
||||
"""The Arabic Kuwait language."""
|
||||
|
||||
ARABIC_LEBANON = (12289, "ar-LB", "The Arabic Lebanon language.")
|
||||
"""The Arabic Lebanon language."""
|
||||
|
||||
ARABIC_LIBYA = (4097, "ar-LY", "The Arabic Libya language.")
|
||||
"""The Arabic Libya language."""
|
||||
|
||||
ARABIC_MOROCCO = (6145, "ar-MA", "The Arabic Morocco language.")
|
||||
"""The Arabic Morocco language."""
|
||||
|
||||
ARABIC_OMAN = (8193, "ar-OM", "The Arabic Oman language.")
|
||||
"""The Arabic Oman language."""
|
||||
|
||||
ARABIC_QATAR = (16385, "ar-QA", "The Arabic Qatar language.")
|
||||
"""The Arabic Qatar language."""
|
||||
|
||||
ARABIC_SYRIA = (10241, "ar-SY", "The Arabic Syria language.")
|
||||
"""The Arabic Syria language."""
|
||||
|
||||
ARABIC_TUNISIA = (7169, "ar-TN", "The Arabic Tunisia language.")
|
||||
"""The Arabic Tunisia language."""
|
||||
|
||||
ARABIC_UAE = (14337, "ar-AE", "The Arabic UAE language.")
|
||||
"""The Arabic UAE language."""
|
||||
|
||||
ARABIC_YEMEN = (9217, "ar-YE", "The Arabic Yemen language.")
|
||||
"""The Arabic Yemen language."""
|
||||
|
||||
ARMENIAN = (1067, "hy-AM", "The Armenian language.")
|
||||
"""The Armenian language."""
|
||||
|
||||
ASSAMESE = (1101, "as-IN", "The Assamese language.")
|
||||
"""The Assamese language."""
|
||||
|
||||
AZERI_CYRILLIC = (2092, "az-AZ", "The Azeri Cyrillic language.")
|
||||
"""The Azeri Cyrillic language."""
|
||||
|
||||
AZERI_LATIN = (1068, "az-Latn-AZ", "The Azeri Latin language.")
|
||||
"""The Azeri Latin language."""
|
||||
|
||||
BASQUE = (1069, "eu-ES", "The Basque language.")
|
||||
"""The Basque language."""
|
||||
|
||||
BELGIAN_DUTCH = (2067, "nl-BE", "The Belgian Dutch language.")
|
||||
"""The Belgian Dutch language."""
|
||||
|
||||
BELGIAN_FRENCH = (2060, "fr-BE", "The Belgian French language.")
|
||||
"""The Belgian French language."""
|
||||
|
||||
BENGALI = (1093, "bn-IN", "The Bengali language.")
|
||||
"""The Bengali language."""
|
||||
|
||||
BOSNIAN = (4122, "hr-BA", "The Bosnian language.")
|
||||
"""The Bosnian language."""
|
||||
|
||||
BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
|
||||
8218,
|
||||
"bs-BA",
|
||||
"The Bosnian Bosnia Herzegovina Cyrillic language.",
|
||||
)
|
||||
"""The Bosnian Bosnia Herzegovina Cyrillic language."""
|
||||
|
||||
BOSNIAN_BOSNIA_HERZEGOVINA_LATIN = (
|
||||
5146,
|
||||
"bs-Latn-BA",
|
||||
"The Bosnian Bosnia Herzegovina Latin language.",
|
||||
)
|
||||
"""The Bosnian Bosnia Herzegovina Latin language."""
|
||||
|
||||
BRAZILIAN_PORTUGUESE = (1046, "pt-BR", "The Brazilian Portuguese language.")
|
||||
"""The Brazilian Portuguese language."""
|
||||
|
||||
BULGARIAN = (1026, "bg-BG", "The Bulgarian language.")
|
||||
"""The Bulgarian language."""
|
||||
|
||||
BURMESE = (1109, "my-MM", "The Burmese language.")
|
||||
"""The Burmese language."""
|
||||
|
||||
BYELORUSSIAN = (1059, "be-BY", "The Byelorussian language.")
|
||||
"""The Byelorussian language."""
|
||||
|
||||
CATALAN = (1027, "ca-ES", "The Catalan language.")
|
||||
"""The Catalan language."""
|
||||
|
||||
CHEROKEE = (1116, "chr-US", "The Cherokee language.")
|
||||
"""The Cherokee language."""
|
||||
|
||||
CHINESE_HONG_KONG_SAR = (3076, "zh-HK", "The Chinese Hong Kong SAR language.")
|
||||
"""The Chinese Hong Kong SAR language."""
|
||||
|
||||
CHINESE_MACAO_SAR = (5124, "zh-MO", "The Chinese Macao SAR language.")
|
||||
"""The Chinese Macao SAR language."""
|
||||
|
||||
CHINESE_SINGAPORE = (4100, "zh-SG", "The Chinese Singapore language.")
|
||||
"""The Chinese Singapore language."""
|
||||
|
||||
CROATIAN = (1050, "hr-HR", "The Croatian language.")
|
||||
"""The Croatian language."""
|
||||
|
||||
CZECH = (1029, "cs-CZ", "The Czech language.")
|
||||
"""The Czech language."""
|
||||
|
||||
DANISH = (1030, "da-DK", "The Danish language.")
|
||||
"""The Danish language."""
|
||||
|
||||
DIVEHI = (1125, "div-MV", "The Divehi language.")
|
||||
"""The Divehi language."""
|
||||
|
||||
DUTCH = (1043, "nl-NL", "The Dutch language.")
|
||||
"""The Dutch language."""
|
||||
|
||||
EDO = (1126, "bin-NG", "The Edo language.")
|
||||
"""The Edo language."""
|
||||
|
||||
ENGLISH_AUS = (3081, "en-AU", "The English AUS language.")
|
||||
"""The English AUS language."""
|
||||
|
||||
ENGLISH_BELIZE = (10249, "en-BZ", "The English Belize language.")
|
||||
"""The English Belize language."""
|
||||
|
||||
ENGLISH_CANADIAN = (4105, "en-CA", "The English Canadian language.")
|
||||
"""The English Canadian language."""
|
||||
|
||||
ENGLISH_CARIBBEAN = (9225, "en-CB", "The English Caribbean language.")
|
||||
"""The English Caribbean language."""
|
||||
|
||||
ENGLISH_INDONESIA = (14345, "en-ID", "The English Indonesia language.")
|
||||
"""The English Indonesia language."""
|
||||
|
||||
ENGLISH_IRELAND = (6153, "en-IE", "The English Ireland language.")
|
||||
"""The English Ireland language."""
|
||||
|
||||
ENGLISH_JAMAICA = (8201, "en-JA", "The English Jamaica language.")
|
||||
"""The English Jamaica language."""
|
||||
|
||||
ENGLISH_NEW_ZEALAND = (5129, "en-NZ", "The English NewZealand language.")
|
||||
"""The English NewZealand language."""
|
||||
|
||||
ENGLISH_PHILIPPINES = (13321, "en-PH", "The English Philippines language.")
|
||||
"""The English Philippines language."""
|
||||
|
||||
ENGLISH_SOUTH_AFRICA = (7177, "en-ZA", "The English South Africa language.")
|
||||
"""The English South Africa language."""
|
||||
|
||||
ENGLISH_TRINIDAD_TOBAGO = (11273, "en-TT", "The English Trinidad Tobago language.")
|
||||
"""The English Trinidad Tobago language."""
|
||||
|
||||
ENGLISH_UK = (2057, "en-GB", "The English UK language.")
|
||||
"""The English UK language."""
|
||||
|
||||
ENGLISH_US = (1033, "en-US", "The English US language.")
|
||||
"""The English US language."""
|
||||
|
||||
ENGLISH_ZIMBABWE = (12297, "en-ZW", "The English Zimbabwe language.")
|
||||
"""The English Zimbabwe language."""
|
||||
|
||||
ESTONIAN = (1061, "et-EE", "The Estonian language.")
|
||||
"""The Estonian language."""
|
||||
|
||||
FAEROESE = (1080, "fo-FO", "The Faeroese language.")
|
||||
"""The Faeroese language."""
|
||||
|
||||
FARSI = (1065, "fa-IR", "The Farsi language.")
|
||||
"""The Farsi language."""
|
||||
|
||||
FILIPINO = (1124, "fil-PH", "The Filipino language.")
|
||||
"""The Filipino language."""
|
||||
|
||||
FINNISH = (1035, "fi-FI", "The Finnish language.")
|
||||
"""The Finnish language."""
|
||||
|
||||
FRANCH_CONGO_DRC = (9228, "fr-CD", "The French Congo DRC language.")
|
||||
"""The French Congo DRC language."""
|
||||
|
||||
FRENCH = (1036, "fr-FR", "The French language.")
|
||||
"""The French language."""
|
||||
|
||||
FRENCH_CAMEROON = (11276, "fr-CM", "The French Cameroon language.")
|
||||
"""The French Cameroon language."""
|
||||
|
||||
FRENCH_CANADIAN = (3084, "fr-CA", "The French Canadian language.")
|
||||
"""The French Canadian language."""
|
||||
|
||||
FRENCH_COTED_IVOIRE = (12300, "fr-CI", "The French Coted Ivoire language.")
|
||||
"""The French Coted Ivoire language."""
|
||||
|
||||
FRENCH_HAITI = (15372, "fr-HT", "The French Haiti language.")
|
||||
"""The French Haiti language."""
|
||||
|
||||
FRENCH_LUXEMBOURG = (5132, "fr-LU", "The French Luxembourg language.")
|
||||
"""The French Luxembourg language."""
|
||||
|
||||
FRENCH_MALI = (13324, "fr-ML", "The French Mali language.")
|
||||
"""The French Mali language."""
|
||||
|
||||
FRENCH_MONACO = (6156, "fr-MC", "The French Monaco language.")
|
||||
"""The French Monaco language."""
|
||||
|
||||
FRENCH_MOROCCO = (14348, "fr-MA", "The French Morocco language.")
|
||||
"""The French Morocco language."""
|
||||
|
||||
FRENCH_REUNION = (8204, "fr-RE", "The French Reunion language.")
|
||||
"""The French Reunion language."""
|
||||
|
||||
FRENCH_SENEGAL = (10252, "fr-SN", "The French Senegal language.")
|
||||
"""The French Senegal language."""
|
||||
|
||||
FRENCH_WEST_INDIES = (7180, "fr-WINDIES", "The French West Indies language.")
|
||||
"""The French West Indies language."""
|
||||
|
||||
FRISIAN_NETHERLANDS = (1122, "fy-NL", "The Frisian Netherlands language.")
|
||||
"""The Frisian Netherlands language."""
|
||||
|
||||
FULFULDE = (1127, "ff-NG", "The Fulfulde language.")
|
||||
"""The Fulfulde language."""
|
||||
|
||||
GAELIC_IRELAND = (2108, "ga-IE", "The Gaelic Ireland language.")
|
||||
"""The Gaelic Ireland language."""
|
||||
|
||||
GAELIC_SCOTLAND = (1084, "en-US", "The Gaelic Scotland language.")
|
||||
"""The Gaelic Scotland language."""
|
||||
|
||||
GALICIAN = (1110, "gl-ES", "The Galician language.")
|
||||
"""The Galician language."""
|
||||
|
||||
GEORGIAN = (1079, "ka-GE", "The Georgian language.")
|
||||
"""The Georgian language."""
|
||||
|
||||
GERMAN = (1031, "de-DE", "The German language.")
|
||||
"""The German language."""
|
||||
|
||||
GERMAN_AUSTRIA = (3079, "de-AT", "The German Austria language.")
|
||||
"""The German Austria language."""
|
||||
|
||||
GERMAN_LIECHTENSTEIN = (5127, "de-LI", "The German Liechtenstein language.")
|
||||
"""The German Liechtenstein language."""
|
||||
|
||||
GERMAN_LUXEMBOURG = (4103, "de-LU", "The German Luxembourg language.")
|
||||
"""The German Luxembourg language."""
|
||||
|
||||
GREEK = (1032, "el-GR", "The Greek language.")
|
||||
"""The Greek language."""
|
||||
|
||||
GUARANI = (1140, "gn-PY", "The Guarani language.")
|
||||
"""The Guarani language."""
|
||||
|
||||
GUJARATI = (1095, "gu-IN", "The Gujarati language.")
|
||||
"""The Gujarati language."""
|
||||
|
||||
HAUSA = (1128, "ha-NG", "The Hausa language.")
|
||||
"""The Hausa language."""
|
||||
|
||||
HAWAIIAN = (1141, "haw-US", "The Hawaiian language.")
|
||||
"""The Hawaiian language."""
|
||||
|
||||
HEBREW = (1037, "he-IL", "The Hebrew language.")
|
||||
"""The Hebrew language."""
|
||||
|
||||
HINDI = (1081, "hi-IN", "The Hindi language.")
|
||||
"""The Hindi language."""
|
||||
|
||||
HUNGARIAN = (1038, "hu-HU", "The Hungarian language.")
|
||||
"""The Hungarian language."""
|
||||
|
||||
IBIBIO = (1129, "ibb-NG", "The Ibibio language.")
|
||||
"""The Ibibio language."""
|
||||
|
||||
ICELANDIC = (1039, "is-IS", "The Icelandic language.")
|
||||
"""The Icelandic language."""
|
||||
|
||||
IGBO = (1136, "ig-NG", "The Igbo language.")
|
||||
"""The Igbo language."""
|
||||
|
||||
INDONESIAN = (1057, "id-ID", "The Indonesian language.")
|
||||
"""The Indonesian language."""
|
||||
|
||||
INUKTITUT = (1117, "iu-Cans-CA", "The Inuktitut language.")
|
||||
"""The Inuktitut language."""
|
||||
|
||||
ITALIAN = (1040, "it-IT", "The Italian language.")
|
||||
"""The Italian language."""
|
||||
|
||||
JAPANESE = (1041, "ja-JP", "The Japanese language.")
|
||||
"""The Japanese language."""
|
||||
|
||||
KANNADA = (1099, "kn-IN", "The Kannada language.")
|
||||
"""The Kannada language."""
|
||||
|
||||
KANURI = (1137, "kr-NG", "The Kanuri language.")
|
||||
"""The Kanuri language."""
|
||||
|
||||
KASHMIRI = (1120, "ks-Arab", "The Kashmiri language.")
|
||||
"""The Kashmiri language."""
|
||||
|
||||
KASHMIRI_DEVANAGARI = (2144, "ks-Deva", "The Kashmiri Devanagari language.")
|
||||
"""The Kashmiri Devanagari language."""
|
||||
|
||||
KAZAKH = (1087, "kk-KZ", "The Kazakh language.")
|
||||
"""The Kazakh language."""
|
||||
|
||||
KHMER = (1107, "kh-KH", "The Khmer language.")
|
||||
"""The Khmer language."""
|
||||
|
||||
KIRGHIZ = (1088, "ky-KG", "The Kirghiz language.")
|
||||
"""The Kirghiz language."""
|
||||
|
||||
KONKANI = (1111, "kok-IN", "The Konkani language.")
|
||||
"""The Konkani language."""
|
||||
|
||||
KOREAN = (1042, "ko-KR", "The Korean language.")
|
||||
"""The Korean language."""
|
||||
|
||||
KYRGYZ = (1088, "ky-KG", "The Kyrgyz language.")
|
||||
"""The Kyrgyz language."""
|
||||
|
||||
LAO = (1108, "lo-LA", "The Lao language.")
|
||||
"""The Lao language."""
|
||||
|
||||
LATIN = (1142, "la-Latn", "The Latin language.")
|
||||
"""The Latin language."""
|
||||
|
||||
LATVIAN = (1062, "lv-LV", "The Latvian language.")
|
||||
"""The Latvian language."""
|
||||
|
||||
LITHUANIAN = (1063, "lt-LT", "The Lithuanian language.")
|
||||
"""The Lithuanian language."""
|
||||
|
||||
MACEDONINAN_FYROM = (1071, "mk-MK", "The Macedonian FYROM language.")
|
||||
"""The Macedonian FYROM language."""
|
||||
|
||||
MALAY_BRUNEI_DARUSSALAM = (2110, "ms-BN", "The Malay Brunei Darussalam language.")
|
||||
"""The Malay Brunei Darussalam language."""
|
||||
|
||||
MALAYALAM = (1100, "ml-IN", "The Malayalam language.")
|
||||
"""The Malayalam language."""
|
||||
|
||||
MALAYSIAN = (1086, "ms-MY", "The Malaysian language.")
|
||||
"""The Malaysian language."""
|
||||
|
||||
MALTESE = (1082, "mt-MT", "The Maltese language.")
|
||||
"""The Maltese language."""
|
||||
|
||||
MANIPURI = (1112, "mni-IN", "The Manipuri language.")
|
||||
"""The Manipuri language."""
|
||||
|
||||
MAORI = (1153, "mi-NZ", "The Maori language.")
|
||||
"""The Maori language."""
|
||||
|
||||
MARATHI = (1102, "mr-IN", "The Marathi language.")
|
||||
"""The Marathi language."""
|
||||
|
||||
MEXICAN_SPANISH = (2058, "es-MX", "The Mexican Spanish language.")
|
||||
"""The Mexican Spanish language."""
|
||||
|
||||
MONGOLIAN = (1104, "mn-MN", "The Mongolian language.")
|
||||
"""The Mongolian language."""
|
||||
|
||||
NEPALI = (1121, "ne-NP", "The Nepali language.")
|
||||
"""The Nepali language."""
|
||||
|
||||
NO_PROOFING = (1024, "en-US", "No proofing.")
|
||||
"""No proofing."""
|
||||
|
||||
NORWEGIAN_BOKMOL = (1044, "nb-NO", "The Norwegian Bokmol language.")
|
||||
"""The Norwegian Bokmol language."""
|
||||
|
||||
NORWEGIAN_NYNORSK = (2068, "nn-NO", "The Norwegian Nynorsk language.")
|
||||
"""The Norwegian Nynorsk language."""
|
||||
|
||||
ORIYA = (1096, "or-IN", "The Oriya language.")
|
||||
"""The Oriya language."""
|
||||
|
||||
OROMO = (1138, "om-Ethi-ET", "The Oromo language.")
|
||||
"""The Oromo language."""
|
||||
|
||||
PASHTO = (1123, "ps-AF", "The Pashto language.")
|
||||
"""The Pashto language."""
|
||||
|
||||
POLISH = (1045, "pl-PL", "The Polish language.")
|
||||
"""The Polish language."""
|
||||
|
||||
PORTUGUESE = (2070, "pt-PT", "The Portuguese language.")
|
||||
"""The Portuguese language."""
|
||||
|
||||
PUNJABI = (1094, "pa-IN", "The Punjabi language.")
|
||||
"""The Punjabi language."""
|
||||
|
||||
QUECHUA_BOLIVIA = (1131, "quz-BO", "The Quechua Bolivia language.")
|
||||
"""The Quechua Bolivia language."""
|
||||
|
||||
QUECHUA_ECUADOR = (2155, "quz-EC", "The Quechua Ecuador language.")
|
||||
"""The Quechua Ecuador language."""
|
||||
|
||||
QUECHUA_PERU = (3179, "quz-PE", "The Quechua Peru language.")
|
||||
"""The Quechua Peru language."""
|
||||
|
||||
RHAETO_ROMANIC = (1047, "rm-CH", "The Rhaeto Romanic language.")
|
||||
"""The Rhaeto Romanic language."""
|
||||
|
||||
ROMANIAN = (1048, "ro-RO", "The Romanian language.")
|
||||
"""The Romanian language."""
|
||||
|
||||
ROMANIAN_MOLDOVA = (2072, "ro-MO", "The Romanian Moldova language.")
|
||||
"""The Romanian Moldova language."""
|
||||
|
||||
RUSSIAN = (1049, "ru-RU", "The Russian language.")
|
||||
"""The Russian language."""
|
||||
|
||||
RUSSIAN_MOLDOVA = (2073, "ru-MO", "The Russian Moldova language.")
|
||||
"""The Russian Moldova language."""
|
||||
|
||||
SAMI_LAPPISH = (1083, "se-NO", "The Sami Lappish language.")
|
||||
"""The Sami Lappish language."""
|
||||
|
||||
SANSKRIT = (1103, "sa-IN", "The Sanskrit language.")
|
||||
"""The Sanskrit language."""
|
||||
|
||||
SEPEDI = (1132, "ns-ZA", "The Sepedi language.")
|
||||
"""The Sepedi language."""
|
||||
|
||||
SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
|
||||
7194,
|
||||
"sr-BA",
|
||||
"The Serbian Bosnia Herzegovina Cyrillic language.",
|
||||
)
|
||||
"""The Serbian Bosnia Herzegovina Cyrillic language."""
|
||||
|
||||
SERBIAN_BOSNIA_HERZEGOVINA_LATIN = (
|
||||
6170,
|
||||
"sr-Latn-BA",
|
||||
"The Serbian Bosnia Herzegovina Latin language.",
|
||||
)
|
||||
"""The Serbian Bosnia Herzegovina Latin language."""
|
||||
|
||||
SERBIAN_CYRILLIC = (3098, "sr-SP", "The Serbian Cyrillic language.")
|
||||
"""The Serbian Cyrillic language."""
|
||||
|
||||
SERBIAN_LATIN = (2074, "sr-Latn-CS", "The Serbian Latin language.")
|
||||
"""The Serbian Latin language."""
|
||||
|
||||
SESOTHO = (1072, "st-ZA", "The Sesotho language.")
|
||||
"""The Sesotho language."""
|
||||
|
||||
SIMPLIFIED_CHINESE = (2052, "zh-CN", "The Simplified Chinese language.")
|
||||
"""The Simplified Chinese language."""
|
||||
|
||||
SINDHI = (1113, "sd-Deva-IN", "The Sindhi language.")
|
||||
"""The Sindhi language."""
|
||||
|
||||
SINDHI_PAKISTAN = (2137, "sd-Arab-PK", "The Sindhi Pakistan language.")
|
||||
"""The Sindhi Pakistan language."""
|
||||
|
||||
SINHALESE = (1115, "si-LK", "The Sinhalese language.")
|
||||
"""The Sinhalese language."""
|
||||
|
||||
SLOVAK = (1051, "sk-SK", "The Slovak language.")
|
||||
"""The Slovak language."""
|
||||
|
||||
SLOVENIAN = (1060, "sl-SI", "The Slovenian language.")
|
||||
"""The Slovenian language."""
|
||||
|
||||
SOMALI = (1143, "so-SO", "The Somali language.")
|
||||
"""The Somali language."""
|
||||
|
||||
SORBIAN = (1070, "wen-DE", "The Sorbian language.")
|
||||
"""The Sorbian language."""
|
||||
|
||||
SPANISH = (1034, "es-ES_tradnl", "The Spanish language.")
|
||||
"""The Spanish language."""
|
||||
|
||||
SPANISH_ARGENTINA = (11274, "es-AR", "The Spanish Argentina language.")
|
||||
"""The Spanish Argentina language."""
|
||||
|
||||
SPANISH_BOLIVIA = (16394, "es-BO", "The Spanish Bolivia language.")
|
||||
"""The Spanish Bolivia language."""
|
||||
|
||||
SPANISH_CHILE = (13322, "es-CL", "The Spanish Chile language.")
|
||||
"""The Spanish Chile language."""
|
||||
|
||||
SPANISH_COLOMBIA = (9226, "es-CO", "The Spanish Colombia language.")
|
||||
"""The Spanish Colombia language."""
|
||||
|
||||
SPANISH_COSTA_RICA = (5130, "es-CR", "The Spanish Costa Rica language.")
|
||||
"""The Spanish Costa Rica language."""
|
||||
|
||||
SPANISH_DOMINICAN_REPUBLIC = (7178, "es-DO", "The Spanish Dominican Republic language.")
|
||||
"""The Spanish Dominican Republic language."""
|
||||
|
||||
SPANISH_ECUADOR = (12298, "es-EC", "The Spanish Ecuador language.")
|
||||
"""The Spanish Ecuador language."""
|
||||
|
||||
SPANISH_EL_SALVADOR = (17418, "es-SV", "The Spanish El Salvador language.")
|
||||
"""The Spanish El Salvador language."""
|
||||
|
||||
SPANISH_GUATEMALA = (4106, "es-GT", "The Spanish Guatemala language.")
|
||||
"""The Spanish Guatemala language."""
|
||||
|
||||
SPANISH_HONDURAS = (18442, "es-HN", "The Spanish Honduras language.")
|
||||
"""The Spanish Honduras language."""
|
||||
|
||||
SPANISH_MODERN_SORT = (3082, "es-ES", "The Spanish Modern Sort language.")
|
||||
"""The Spanish Modern Sort language."""
|
||||
|
||||
SPANISH_NICARAGUA = (19466, "es-NI", "The Spanish Nicaragua language.")
|
||||
"""The Spanish Nicaragua language."""
|
||||
|
||||
SPANISH_PANAMA = (6154, "es-PA", "The Spanish Panama language.")
|
||||
"""The Spanish Panama language."""
|
||||
|
||||
SPANISH_PARAGUAY = (15370, "es-PY", "The Spanish Paraguay language.")
|
||||
"""The Spanish Paraguay language."""
|
||||
|
||||
SPANISH_PERU = (10250, "es-PE", "The Spanish Peru language.")
|
||||
"""The Spanish Peru language."""
|
||||
|
||||
SPANISH_PUERTO_RICO = (20490, "es-PR", "The Spanish Puerto Rico language.")
|
||||
"""The Spanish Puerto Rico language."""
|
||||
|
||||
SPANISH_URUGUAY = (14346, "es-UR", "The Spanish Uruguay language.")
|
||||
"""The Spanish Uruguay language."""
|
||||
|
||||
SPANISH_VENEZUELA = (8202, "es-VE", "The Spanish Venezuela language.")
|
||||
"""The Spanish Venezuela language."""
|
||||
|
||||
SUTU = (1072, "st-ZA", "The Sutu language.")
|
||||
"""The Sutu language."""
|
||||
|
||||
SWAHILI = (1089, "sw-KE", "The Swahili language.")
|
||||
"""The Swahili language."""
|
||||
|
||||
SWEDISH = (1053, "sv-SE", "The Swedish language.")
|
||||
"""The Swedish language."""
|
||||
|
||||
SWEDISH_FINLAND = (2077, "sv-FI", "The Swedish Finland language.")
|
||||
"""The Swedish Finland language."""
|
||||
|
||||
SWISS_FRENCH = (4108, "fr-CH", "The Swiss French language.")
|
||||
"""The Swiss French language."""
|
||||
|
||||
SWISS_GERMAN = (2055, "de-CH", "The Swiss German language.")
|
||||
"""The Swiss German language."""
|
||||
|
||||
SWISS_ITALIAN = (2064, "it-CH", "The Swiss Italian language.")
|
||||
"""The Swiss Italian language."""
|
||||
|
||||
SYRIAC = (1114, "syr-SY", "The Syriac language.")
|
||||
"""The Syriac language."""
|
||||
|
||||
TAJIK = (1064, "tg-TJ", "The Tajik language.")
|
||||
"""The Tajik language."""
|
||||
|
||||
TAMAZIGHT = (1119, "tzm-Arab-MA", "The Tamazight language.")
|
||||
"""The Tamazight language."""
|
||||
|
||||
TAMAZIGHT_LATIN = (2143, "tmz-DZ", "The Tamazight Latin language.")
|
||||
"""The Tamazight Latin language."""
|
||||
|
||||
TAMIL = (1097, "ta-IN", "The Tamil language.")
|
||||
"""The Tamil language."""
|
||||
|
||||
TATAR = (1092, "tt-RU", "The Tatar language.")
|
||||
"""The Tatar language."""
|
||||
|
||||
TELUGU = (1098, "te-IN", "The Telugu language.")
|
||||
"""The Telugu language."""
|
||||
|
||||
THAI = (1054, "th-TH", "The Thai language.")
|
||||
"""The Thai language."""
|
||||
|
||||
TIBETAN = (1105, "bo-CN", "The Tibetan language.")
|
||||
"""The Tibetan language."""
|
||||
|
||||
TIGRIGNA_ERITREA = (2163, "ti-ER", "The Tigrigna Eritrea language.")
|
||||
"""The Tigrigna Eritrea language."""
|
||||
|
||||
TIGRIGNA_ETHIOPIC = (1139, "ti-ET", "The Tigrigna Ethiopic language.")
|
||||
"""The Tigrigna Ethiopic language."""
|
||||
|
||||
TRADITIONAL_CHINESE = (1028, "zh-TW", "The Traditional Chinese language.")
|
||||
"""The Traditional Chinese language."""
|
||||
|
||||
TSONGA = (1073, "ts-ZA", "The Tsonga language.")
|
||||
"""The Tsonga language."""
|
||||
|
||||
TSWANA = (1074, "tn-ZA", "The Tswana language.")
|
||||
"""The Tswana language."""
|
||||
|
||||
TURKISH = (1055, "tr-TR", "The Turkish language.")
|
||||
"""The Turkish language."""
|
||||
|
||||
TURKMEN = (1090, "tk-TM", "The Turkmen language.")
|
||||
"""The Turkmen language."""
|
||||
|
||||
UKRAINIAN = (1058, "uk-UA", "The Ukrainian language.")
|
||||
"""The Ukrainian language."""
|
||||
|
||||
URDU = (1056, "ur-PK", "The Urdu language.")
|
||||
"""The Urdu language."""
|
||||
|
||||
UZBEK_CYRILLIC = (2115, "uz-UZ", "The Uzbek Cyrillic language.")
|
||||
"""The Uzbek Cyrillic language."""
|
||||
|
||||
UZBEK_LATIN = (1091, "uz-Latn-UZ", "The Uzbek Latin language.")
|
||||
"""The Uzbek Latin language."""
|
||||
|
||||
VENDA = (1075, "ve-ZA", "The Venda language.")
|
||||
"""The Venda language."""
|
||||
|
||||
VIETNAMESE = (1066, "vi-VN", "The Vietnamese language.")
|
||||
"""The Vietnamese language."""
|
||||
|
||||
WELSH = (1106, "cy-GB", "The Welsh language.")
|
||||
"""The Welsh language."""
|
||||
|
||||
XHOSA = (1076, "xh-ZA", "The Xhosa language.")
|
||||
"""The Xhosa language."""
|
||||
|
||||
YI = (1144, "ii-CN", "The Yi language.")
|
||||
"""The Yi language."""
|
||||
|
||||
YIDDISH = (1085, "yi-Hebr", "The Yiddish language.")
|
||||
"""The Yiddish language."""
|
||||
|
||||
YORUBA = (1130, "yo-NG", "The Yoruba language.")
|
||||
"""The Yoruba language."""
|
||||
|
||||
ZULU = (1077, "zu-ZA", "The Zulu language.")
|
||||
"""The Zulu language."""
|
||||
|
||||
MIXED = (-2, "", "More than one language in specified range (read-only).")
|
||||
"""More than one language in specified range (read-only)."""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
"""Enumerations used by text and related objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pptx.enum.base import BaseEnum, BaseXmlEnum
|
||||
|
||||
|
||||
class MSO_AUTO_SIZE(BaseEnum):
|
||||
"""Determines the type of automatic sizing allowed.
|
||||
|
||||
The following names can be used to specify the automatic sizing behavior used to fit a shape's
|
||||
text within the shape bounding box, for example::
|
||||
|
||||
from pptx.enum.text import MSO_AUTO_SIZE
|
||||
|
||||
shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
|
||||
|
||||
The word-wrap setting of the text frame interacts with the auto-size setting to determine the
|
||||
specific auto-sizing behavior.
|
||||
|
||||
Note that `TextFrame.auto_size` can also be set to |None|, which removes the auto size setting
|
||||
altogether. This causes the setting to be inherited, either from the layout placeholder, in the
|
||||
case of a placeholder shape, or from the theme.
|
||||
|
||||
MS API Name: `MsoAutoSize`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx
|
||||
"""
|
||||
|
||||
NONE = (
|
||||
0,
|
||||
"No automatic sizing of the shape or text will be done.\n\nText can freely extend beyond"
|
||||
" the horizontal and vertical edges of the shape bounding box.",
|
||||
)
|
||||
"""No automatic sizing of the shape or text will be done.
|
||||
|
||||
Text can freely extend beyond the horizontal and vertical edges of the shape bounding box.
|
||||
"""
|
||||
|
||||
SHAPE_TO_FIT_TEXT = (
|
||||
1,
|
||||
"The shape height and possibly width are adjusted to fit the text.\n\nNote this setting"
|
||||
" interacts with the TextFrame.word_wrap property setting. If word wrap is turned on,"
|
||||
" only the height of the shape will be adjusted; soft line breaks will be used to fit the"
|
||||
" text horizontally.",
|
||||
)
|
||||
"""The shape height and possibly width are adjusted to fit the text.
|
||||
|
||||
Note this setting interacts with the TextFrame.word_wrap property setting. If word wrap is
|
||||
turned on, only the height of the shape will be adjusted; soft line breaks will be used to fit
|
||||
the text horizontally.
|
||||
"""
|
||||
|
||||
TEXT_TO_FIT_SHAPE = (
|
||||
2,
|
||||
"The font size is reduced as necessary to fit the text within the shape.",
|
||||
)
|
||||
"""The font size is reduced as necessary to fit the text within the shape."""
|
||||
|
||||
MIXED = (-2, "Return value only; indicates a combination of automatic sizing schemes are used.")
|
||||
"""Return value only; indicates a combination of automatic sizing schemes are used."""
|
||||
|
||||
|
||||
class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum):
|
||||
"""
|
||||
Indicates the type of underline for text. Used with
|
||||
:attr:`.Font.underline` to specify the style of text underlining.
|
||||
|
||||
Alias: ``MSO_UNDERLINE``
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.text import MSO_UNDERLINE
|
||||
|
||||
run.font.underline = MSO_UNDERLINE.DOUBLE_LINE
|
||||
|
||||
MS API Name: `MsoTextUnderlineType`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/aa432699.aspx
|
||||
"""
|
||||
|
||||
NONE = (0, "none", "Specifies no underline.")
|
||||
"""Specifies no underline."""
|
||||
|
||||
DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.")
|
||||
"""Specifies a dash underline."""
|
||||
|
||||
DASH_LINE = (7, "dash", "Specifies a dash line underline.")
|
||||
"""Specifies a dash line underline."""
|
||||
|
||||
DASH_LONG_HEAVY_LINE = (10, "dashLongHeavy", "Specifies a long heavy line underline.")
|
||||
"""Specifies a long heavy line underline."""
|
||||
|
||||
DASH_LONG_LINE = (9, "dashLong", "Specifies a dashed long line underline.")
|
||||
"""Specifies a dashed long line underline."""
|
||||
|
||||
DOT_DASH_HEAVY_LINE = (12, "dotDashHeavy", "Specifies a dot dash heavy line underline.")
|
||||
"""Specifies a dot dash heavy line underline."""
|
||||
|
||||
DOT_DASH_LINE = (11, "dotDash", "Specifies a dot dash line underline.")
|
||||
"""Specifies a dot dash line underline."""
|
||||
|
||||
DOT_DOT_DASH_HEAVY_LINE = (
|
||||
14,
|
||||
"dotDotDashHeavy",
|
||||
"Specifies a dot dot dash heavy line underline.",
|
||||
)
|
||||
"""Specifies a dot dot dash heavy line underline."""
|
||||
|
||||
DOT_DOT_DASH_LINE = (13, "dotDotDash", "Specifies a dot dot dash line underline.")
|
||||
"""Specifies a dot dot dash line underline."""
|
||||
|
||||
DOTTED_HEAVY_LINE = (6, "dottedHeavy", "Specifies a dotted heavy line underline.")
|
||||
"""Specifies a dotted heavy line underline."""
|
||||
|
||||
DOTTED_LINE = (5, "dotted", "Specifies a dotted line underline.")
|
||||
"""Specifies a dotted line underline."""
|
||||
|
||||
DOUBLE_LINE = (3, "dbl", "Specifies a double line underline.")
|
||||
"""Specifies a double line underline."""
|
||||
|
||||
HEAVY_LINE = (4, "heavy", "Specifies a heavy line underline.")
|
||||
"""Specifies a heavy line underline."""
|
||||
|
||||
SINGLE_LINE = (2, "sng", "Specifies a single line underline.")
|
||||
"""Specifies a single line underline."""
|
||||
|
||||
WAVY_DOUBLE_LINE = (17, "wavyDbl", "Specifies a wavy double line underline.")
|
||||
"""Specifies a wavy double line underline."""
|
||||
|
||||
WAVY_HEAVY_LINE = (16, "wavyHeavy", "Specifies a wavy heavy line underline.")
|
||||
"""Specifies a wavy heavy line underline."""
|
||||
|
||||
WAVY_LINE = (15, "wavy", "Specifies a wavy line underline.")
|
||||
"""Specifies a wavy line underline."""
|
||||
|
||||
WORDS = (1, "words", "Specifies underlining words.")
|
||||
"""Specifies underlining words."""
|
||||
|
||||
MIXED = (-2, "", "Specifies a mix of underline types (read-only).")
|
||||
"""Specifies a mix of underline types (read-only)."""
|
||||
|
||||
|
||||
MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE
|
||||
|
||||
|
||||
class MSO_VERTICAL_ANCHOR(BaseXmlEnum):
|
||||
"""Specifies the vertical alignment of text in a text frame.
|
||||
|
||||
Used with the `.vertical_anchor` property of the |TextFrame| object. Note that the
|
||||
`vertical_anchor` property can also have the value None, indicating there is no directly
|
||||
specified vertical anchor setting and its effective value is inherited from its placeholder if
|
||||
it has one or from the theme. |None| may also be assigned to remove an explicitly specified
|
||||
vertical anchor setting.
|
||||
|
||||
MS API Name: `MsoVerticalAnchor`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff865255.aspx
|
||||
"""
|
||||
|
||||
TOP = (1, "t", "Aligns text to top of text frame")
|
||||
"""Aligns text to top of text frame"""
|
||||
|
||||
MIDDLE = (3, "ctr", "Centers text vertically")
|
||||
"""Centers text vertically"""
|
||||
|
||||
BOTTOM = (4, "b", "Aligns text to bottom of text frame")
|
||||
"""Aligns text to bottom of text frame"""
|
||||
|
||||
MIXED = (-2, "", "Return value only; indicates a combination of the other states.")
|
||||
"""Return value only; indicates a combination of the other states."""
|
||||
|
||||
|
||||
MSO_ANCHOR = MSO_VERTICAL_ANCHOR
|
||||
|
||||
|
||||
class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):
|
||||
"""Specifies the horizontal alignment for one or more paragraphs.
|
||||
|
||||
Alias: `PP_ALIGN`
|
||||
|
||||
Example::
|
||||
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
|
||||
shape.paragraphs[0].alignment = PP_ALIGN.CENTER
|
||||
|
||||
MS API Name: `PpParagraphAlignment`
|
||||
|
||||
http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15).aspx
|
||||
"""
|
||||
|
||||
CENTER = (2, "ctr", "Center align")
|
||||
"""Center align"""
|
||||
|
||||
DISTRIBUTE = (
|
||||
5,
|
||||
"dist",
|
||||
"Evenly distributes e.g. Japanese characters from left to right within a line",
|
||||
)
|
||||
"""Evenly distributes e.g. Japanese characters from left to right within a line"""
|
||||
|
||||
JUSTIFY = (
|
||||
4,
|
||||
"just",
|
||||
"Justified, i.e. each line both begins and ends at the margin.\n\nSpacing between words"
|
||||
" is adjusted such that the line exactly fills the width of the paragraph.",
|
||||
)
|
||||
"""Justified, i.e. each line both begins and ends at the margin.
|
||||
|
||||
Spacing between words is adjusted such that the line exactly fills the width of the paragraph.
|
||||
"""
|
||||
|
||||
JUSTIFY_LOW = (7, "justLow", "Justify using a small amount of space between words.")
|
||||
"""Justify using a small amount of space between words."""
|
||||
|
||||
LEFT = (1, "l", "Left aligned")
|
||||
"""Left aligned"""
|
||||
|
||||
RIGHT = (3, "r", "Right aligned")
|
||||
"""Right aligned"""
|
||||
|
||||
THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed")
|
||||
"""Thai distributed"""
|
||||
|
||||
MIXED = (-2, "", "Multiple alignments are present in a set of paragraphs (read-only).")
|
||||
"""Multiple alignments are present in a set of paragraphs (read-only)."""
|
||||
|
||||
|
||||
PP_ALIGN = PP_PARAGRAPH_ALIGNMENT
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Exceptions used with python-pptx.
|
||||
|
||||
The base exception class is PythonPptxError.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class PythonPptxError(Exception):
|
||||
"""Generic error class."""
|
||||
|
||||
|
||||
class PackageNotFoundError(PythonPptxError):
|
||||
"""
|
||||
Raised when a package cannot be found at the specified path.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidXmlError(PythonPptxError):
|
||||
"""
|
||||
Raised when a value is encountered in the XML that is not valid according
|
||||
to the schema.
|
||||
"""
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Objects related to images, audio, and video."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
from typing import IO
|
||||
|
||||
from pptx.opc.constants import CONTENT_TYPE as CT
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
|
||||
class Video(object):
|
||||
"""Immutable value object representing a video such as MP4."""
|
||||
|
||||
def __init__(self, blob: bytes, mime_type: str | None, filename: str | None):
|
||||
super(Video, self).__init__()
|
||||
self._blob = blob
|
||||
self._mime_type = mime_type
|
||||
self._filename = filename
|
||||
|
||||
@classmethod
|
||||
def from_blob(cls, blob: bytes, mime_type: str | None, filename: str | None = None):
|
||||
"""Return a new |Video| object loaded from image binary in *blob*."""
|
||||
return cls(blob, mime_type, filename)
|
||||
|
||||
@classmethod
|
||||
def from_path_or_file_like(cls, movie_file: str | IO[bytes], mime_type: str | None) -> Video:
|
||||
"""Return a new |Video| object containing video in *movie_file*.
|
||||
|
||||
*movie_file* can be either a path (string) or a file-like
|
||||
(e.g. StringIO) object.
|
||||
"""
|
||||
if isinstance(movie_file, str):
|
||||
# treat movie_file as a path
|
||||
with open(movie_file, "rb") as f:
|
||||
blob = f.read()
|
||||
filename = os.path.basename(movie_file)
|
||||
else:
|
||||
# assume movie_file is a file-like object
|
||||
blob = movie_file.read()
|
||||
filename = None
|
||||
|
||||
return cls.from_blob(blob, mime_type, filename)
|
||||
|
||||
@property
|
||||
def blob(self):
|
||||
"""The bytestream of the media "file"."""
|
||||
return self._blob
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""MIME-type of this media, e.g. `'video/mp4'`."""
|
||||
return self._mime_type
|
||||
|
||||
@property
|
||||
def ext(self):
|
||||
"""Return the file extension for this video, e.g. 'mp4'.
|
||||
|
||||
The extension is that from the actual filename if known. Otherwise
|
||||
it is the lowercase canonical extension for the video's MIME type.
|
||||
'vid' is used if the MIME type is 'video/unknown'.
|
||||
"""
|
||||
if self._filename:
|
||||
return os.path.splitext(self._filename)[1].lstrip(".")
|
||||
return {
|
||||
CT.ASF: "asf",
|
||||
CT.AVI: "avi",
|
||||
CT.MOV: "mov",
|
||||
CT.MP4: "mp4",
|
||||
CT.MPG: "mpg",
|
||||
CT.MS_VIDEO: "avi",
|
||||
CT.SWF: "swf",
|
||||
CT.WMV: "wmv",
|
||||
CT.X_MS_VIDEO: "avi",
|
||||
}.get(self._mime_type, "vid")
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Return a filename.ext string appropriate to this video.
|
||||
|
||||
The base filename from the original path is used if this image was
|
||||
loaded from the filesystem. If no filename is available, such as when
|
||||
the video object is created from an in-memory stream, the string
|
||||
'movie.{ext}' is used where 'ext' is suitable to the video format,
|
||||
such as 'mp4'.
|
||||
"""
|
||||
if self._filename is not None:
|
||||
return self._filename
|
||||
return "movie.%s" % self.ext
|
||||
|
||||
@lazyproperty
|
||||
def sha1(self):
|
||||
"""The SHA1 hash digest for the binary "file" of this video.
|
||||
|
||||
Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
|
||||
"""
|
||||
return hashlib.sha1(self._blob).hexdigest()
|
||||
|
||||
|
||||
SPEAKER_IMAGE_BYTES = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAHgAAAA3CAYAAADHao5rAAAACXBIWXMAAAsTAAALEwEAmpw"
|
||||
"YAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUh"
|
||||
"UIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74"
|
||||
"Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz"
|
||||
"/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEB"
|
||||
"GAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVo"
|
||||
"pFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8"
|
||||
"lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wA"
|
||||
"AKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qI"
|
||||
"l7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X"
|
||||
"48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5Em"
|
||||
"ozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgD"
|
||||
"gGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/x"
|
||||
"gNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKL"
|
||||
"yBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h"
|
||||
"1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP"
|
||||
"2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0I"
|
||||
"gYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iE"
|
||||
"PENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG"
|
||||
"+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1"
|
||||
"mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAc"
|
||||
"YZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81"
|
||||
"XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgs"
|
||||
"V/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx"
|
||||
"+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5"
|
||||
"Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+h"
|
||||
"x9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGj"
|
||||
"UYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb"
|
||||
"15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZ"
|
||||
"nw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFD"
|
||||
"pWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbx"
|
||||
"t3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvf"
|
||||
"rH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+"
|
||||
"F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrS"
|
||||
"FoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6R"
|
||||
"JZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3i"
|
||||
"C+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtG"
|
||||
"I2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQq"
|
||||
"ohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKO"
|
||||
"ZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2"
|
||||
"Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhT"
|
||||
"bF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319k"
|
||||
"XbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/"
|
||||
"T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr"
|
||||
"60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRpt"
|
||||
"TmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752"
|
||||
"PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca"
|
||||
"7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf"
|
||||
"9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L"
|
||||
"96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV"
|
||||
"70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAe"
|
||||
"iUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAACJ5JREFUeNrsm19oW9cdx7/n"
|
||||
"/tO9uleS9ceWZMdO68Su0jiEmNCGLVsDg+J16xhkYx0UM1ayvS19SQZ7WfK6PWR7Gax0S9n"
|
||||
"24JGFlbGNPIRgEkKWZC1u6rizHMXxv1iWJdnWvbqS7p9z9uDokqzs1bWd84GDQEigez465/"
|
||||
"x+v3MOYYyBs3shXDAXzOGCOVwwhwvmcMEcLpjDBXPBHC6YwwVzuOAt4dGjRy/MzMwMmqZpS"
|
||||
"JLkZbPZYn9//8NkMlnmgncwt27d+tKlS5e+W6lUEolEYjQUCoExBsuy4Lru+319fXMjIyNX"
|
||||
"jh49+m8ueAfRarXUCxcuvHvv3r3DR44ceavRaMCyLDDGIAgCFEVBOBwGIQSFQuH9PXv2PD5"
|
||||
"16tRvu7u7H3PB2xzXdZXz58//vFKp/Gzfvn1YXFyEYRgwDAOSJEEQBDDG4LouPM+DpmnwPA"
|
||||
"/5fP73o6Ojf3zttdfGueBtzAcffPCD8fHxi0NDQ1hZWUE6nYYoiiCEQBCEZ14JIfA8D77vA"
|
||||
"wAmJib+cPLkyctvvvnm33ZLf0i7Se7s7Gz/tWvXvpbL5bC+vo5sNgtFUTb/yU/EthshBAAg"
|
||||
"yzIAoNls4tChQ6OXL1+GKIreG2+88U8ueJsxPj5+QlXVt0OhEGRZhqIoEEXxGbFPC6aUBuJ"
|
||||
"VVYVlWThw4MDo2NiYkMlkisPDwx/v9D4Rdotc0zSj9+7dO5RMJgEA4XAYkiRBkqRA9tNNlu"
|
||||
"VArKIokCQJ8Xgc8Xgc+/fvf/u999778fr6egcXvE0ol8spx3HeNQwDoihClmXIsgxRFCFJ0"
|
||||
"ucEE0KgqipCodAz0pPJJFKpFGKx2I8uXrz4Qy54m1CpVBK+70MUxUDW0yO1Lbz9XnuUPz26"
|
||||
"25/JZDLo7OzExMTE4cnJySG+Bm8DarVa1Pd9EEIgy3IwPYuiCFEANFWEIGw2x6PQNA2qqqK"
|
||||
"dRTDGwBgDpTSQXKvVRj/88MOZoaGhSS74C8a27XA75ZEk6YloBYoswHQNfPy4Fx4JoTdmYj"
|
||||
"BRhSozsP+ZwCil8DwPlFIkk0kkEglMTEwM5PP5wcHBwTwX/AXhOI5i23bY930wxqAoymbVi"
|
||||
"jA0/DD+mj8C048iqjMUHYbFRg0n+uaQ0FzQJ5IdxwGlNFi/AaCzsxPpdHr0+vXrN3aq4F2x"
|
||||
"Bs/Pz/eFQqE/t0egIAjQdR2SQDFVzmKlHoEme1BEH3uyDJFsHFdnX4SLECRRhOM4EAQBhmF"
|
||||
"A1/UgvUomk4jH4/j0008PeZ4nccFbTKPRCH/00UdHi8Viph1gNZtNUEqhKAp0XUfRNMAYQC"
|
||||
"ng+0DNAhp1H4s1A3fmkwhrMmKxGGKxGFRVDdZvWZYRiUQQjUZRq9V+Mjc39wIXvIWsra0lx"
|
||||
"sfHTzx48OAuY+yGKIpQVRX1eh2O4wQBFmMUjsvQ8oCWC5RWGWZmKVyX4e58DLarIBzWnomy"
|
||||
"23mxoiiIRqPQdR2FQqGfr8FbhOu6SrFYzJim+Y9mswnHcSCKImKxGFZWVmBZFgRBgCQTpPU"
|
||||
"6LNOCquhgjMERAUIAnzJUWgrmqzJeTclouRSUUrQj8XbQZRgGwuEwFhYW+vgI3iJarZbS2d"
|
||||
"l5v1Qqwfd9uK4LAEgmk2g0GlhfX99cV0UFL2dNCLSGmtlEvUFg2oBpA5YNmHWCikmgKNLnc"
|
||||
"uH2VN2udFWr1QQXvEV4nicxxlCv19FsNoOtv0QiAUVRsLq6ikqlAtel2JvycOxFCyulFZh1"
|
||||
"CqtBNuXaBPV6EyHRhyAIaLVaQST99MZEWzQPsrZ2BKu2bYMQgmq1Cs/z4DgOwuEwMpkMSqU"
|
||||
"SlpeXwQBoegTfe9XEgayDhaUHWNuoo1YHKus1hEgVuR4GLRxFPB6HrusQRRGU0uBwQLsAsl"
|
||||
"O3VXdskNVsNtHR0YHl5WU0Gg24rgvGGLLZLCilWFpawuLiIggR0d1l4KffMPH6yz5a5iNUV"
|
||||
"qcR8ot4+9UKcv0pCOLmlBwKhaBpGjRNC1Kl9p9H07QmD7K2CFVVm2tra0in07AsCysrK0il"
|
||||
"UnBdF5FIBD09PVhYWMDDhw+RyWTQ07MH/X0qznyripNFhnKNYU8SeOVwL6Idm9+jdFNm+yB"
|
||||
"AO9CybRumaeLw4cOPueAtIhKJ1Hzf/4phGDe6urowOTmJY8eOBbXk9jRdKBQQiUTQ0dGBeD"
|
||||
"yOSCyB4z0aZEmErKhoNDfX7v83BXueh2q1ilKpBF7J2sofLQi0p6dn0bZtDAwMYGNjA1NTU"
|
||||
"/B9H61WC4qioK+vDxsbG5iZmcHdu3dh2zZEUUTL8QFBAYgIQSAolUrBsR1KadCe5NpYXl6G"
|
||||
"qqp/yuVy/+GCt5C9e/fOy7L8dVmWkcvlMD09jc8++yxYM3VdR29vL+bn53H//n3cunULtm0"
|
||||
"HGwqu6wY169XVVTiOE4hmjKHRaGB2dhaFQgHHjx+/EQ6H7Z3YT+K5c+d2pGBCCOvu7n48NT"
|
||||
"X1gFL6bcMwUCgUUCqVgkpUu/Q4NzeHZrOJVquFSCQCwzCCIzuSJMH3fViWBUII2ulXoVDAn"
|
||||
"Tt3UC6X/3L27NlfaJrW4IK3GFmW3ZdeeilvWda/HMe5u2/fvhFFUWCa5m/279///vDw8K8y"
|
||||
"mczfCSH56enpr5bL5UC0IAhPSpksCKparRbK5TJmZmZw+/ZtfPLJJ1fPnDnzy506PQO76Nh"
|
||||
"stVpNFIvFDGNMSKfTxVQqVX66tHnp0qXvjI2NfZ8Q8s3e3l709/ejq6sL0WgUoVAIruuiVq"
|
||||
"thaWkJ09PTWFpaunL69Olfj4yMXNnJ/fJcXT7L5/ODY2Njb928efPLoii+3t5IkGUZnufBN"
|
||||
"E2sra1dHR4e/vidd9753cDAQH6nP/Nzebtwdna2//bt269MTk4eKpVKXZ7nSbquW7lcbvrE"
|
||||
"iRPjBw8enNwtz/rcXx9ljAlPypJ0Nz4fvx+8y+GCuWAOF8zhgjlcMIcL5nDBHC6YC+ZwwRw"
|
||||
"umLMN+O8AX65uqCMleo4AAAAASUVORK5CYII="
|
||||
)
|
||||
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,331 @@
|
||||
"""Constant values related to the Open Packaging Convention.
|
||||
|
||||
In particular, this includes content (MIME) types and relationship types.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class CONTENT_TYPE:
|
||||
"""Content type URIs (like MIME-types) that specify a part's format."""
|
||||
|
||||
ASF = "video/x-ms-asf"
|
||||
AVI = "video/avi"
|
||||
BMP = "image/bmp"
|
||||
DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||
DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
|
||||
DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml"
|
||||
DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"
|
||||
DML_DIAGRAM_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml"
|
||||
DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml"
|
||||
DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml"
|
||||
GIF = "image/gif"
|
||||
INK = "application/inkml+xml"
|
||||
JPEG = "image/jpeg"
|
||||
MOV = "video/quicktime"
|
||||
MP4 = "video/mp4"
|
||||
MPG = "video/mpeg"
|
||||
MS_PHOTO = "image/vnd.ms-photo"
|
||||
MS_VIDEO = "video/msvideo"
|
||||
OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml"
|
||||
OFC_CHART_EX = "application/vnd.ms-office.chartex+xml"
|
||||
OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml"
|
||||
OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
|
||||
OFC_CUSTOM_XML_PROPERTIES = (
|
||||
"application/vnd.openxmlformats-officedocument.customXmlProperties+xml"
|
||||
)
|
||||
OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml"
|
||||
OFC_EXTENDED_PROPERTIES = (
|
||||
"application/vnd.openxmlformats-officedocument.extended-properties+xml"
|
||||
)
|
||||
OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject"
|
||||
OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package"
|
||||
OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
|
||||
OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml"
|
||||
OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
||||
OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml"
|
||||
OPC_DIGITAL_SIGNATURE_CERTIFICATE = (
|
||||
"application/vnd.openxmlformats-package.digital-signature-certificate"
|
||||
)
|
||||
OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin"
|
||||
OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = (
|
||||
"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"
|
||||
)
|
||||
OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml"
|
||||
PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml"
|
||||
PML_COMMENT_AUTHORS = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml"
|
||||
)
|
||||
PML_HANDOUT_MASTER = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml"
|
||||
)
|
||||
PML_NOTES_MASTER = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
|
||||
)
|
||||
PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
|
||||
PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
PML_PRESENTATION_MAIN = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
|
||||
)
|
||||
PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml"
|
||||
PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"
|
||||
PML_PRINTER_SETTINGS = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.printerSettings"
|
||||
)
|
||||
PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"
|
||||
PML_SLIDESHOW_MAIN = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml"
|
||||
)
|
||||
PML_SLIDE_LAYOUT = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
|
||||
)
|
||||
PML_SLIDE_MASTER = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"
|
||||
)
|
||||
PML_SLIDE_UPDATE_INFO = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml"
|
||||
)
|
||||
PML_TABLE_STYLES = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"
|
||||
)
|
||||
PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml"
|
||||
PML_TEMPLATE_MAIN = (
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml"
|
||||
)
|
||||
PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"
|
||||
PNG = "image/png"
|
||||
SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
|
||||
SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
|
||||
SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
|
||||
SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml"
|
||||
SML_CUSTOM_PROPERTY = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty"
|
||||
)
|
||||
SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml"
|
||||
SML_EXTERNAL_LINK = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"
|
||||
)
|
||||
SML_PIVOT_CACHE_DEFINITION = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
|
||||
)
|
||||
SML_PIVOT_CACHE_RECORDS = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
|
||||
)
|
||||
SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
||||
SML_PRINTER_SETTINGS = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"
|
||||
)
|
||||
SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml"
|
||||
SML_REVISION_HEADERS = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml"
|
||||
)
|
||||
SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml"
|
||||
SML_SHARED_STRINGS = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
|
||||
)
|
||||
SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
||||
SML_SHEET_METADATA = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"
|
||||
)
|
||||
SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
|
||||
SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
|
||||
SML_TABLE_SINGLE_CELLS = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml"
|
||||
)
|
||||
SML_TEMPLATE_MAIN = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
|
||||
)
|
||||
SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml"
|
||||
SML_VOLATILE_DEPENDENCIES = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml"
|
||||
)
|
||||
SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
||||
SWF = "application/x-shockwave-flash"
|
||||
TIFF = "image/tiff"
|
||||
VIDEO = "video/unknown"
|
||||
WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
|
||||
WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
WML_DOCUMENT_GLOSSARY = (
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"
|
||||
)
|
||||
WML_DOCUMENT_MAIN = (
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
|
||||
)
|
||||
WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
|
||||
WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"
|
||||
WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
|
||||
WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"
|
||||
WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
|
||||
WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"
|
||||
WML_PRINTER_SETTINGS = (
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings"
|
||||
)
|
||||
WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"
|
||||
WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"
|
||||
WML_WEB_SETTINGS = (
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"
|
||||
)
|
||||
WMV = "video/x-ms-wmv"
|
||||
XML = "application/xml"
|
||||
X_EMF = "image/x-emf"
|
||||
X_FONTDATA = "application/x-fontdata"
|
||||
X_FONT_TTF = "application/x-font-ttf"
|
||||
X_MS_VIDEO = "video/x-msvideo"
|
||||
X_WMF = "image/x-wmf"
|
||||
|
||||
|
||||
class NAMESPACE:
|
||||
"""Constant values for OPC XML namespaces"""
|
||||
|
||||
DML_WORDPROCESSING_DRAWING = (
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
)
|
||||
OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
||||
OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types"
|
||||
WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
class RELATIONSHIP_TARGET_MODE:
|
||||
"""Open XML relationship target modes"""
|
||||
|
||||
EXTERNAL = "External"
|
||||
INTERNAL = "Internal"
|
||||
|
||||
|
||||
class RELATIONSHIP_TYPE:
|
||||
AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"
|
||||
A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
|
||||
CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain"
|
||||
CERTIFICATE = (
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
|
||||
"re/certificate"
|
||||
)
|
||||
CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
|
||||
CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
|
||||
CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle"
|
||||
CHART_USER_SHAPES = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes"
|
||||
)
|
||||
COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
COMMENT_AUTHORS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"
|
||||
)
|
||||
CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections"
|
||||
CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control"
|
||||
CORE_PROPERTIES = (
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
|
||||
)
|
||||
CUSTOM_PROPERTIES = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
|
||||
)
|
||||
CUSTOM_PROPERTY = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty"
|
||||
)
|
||||
CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"
|
||||
CUSTOM_XML_PROPS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps"
|
||||
)
|
||||
DIAGRAM_COLORS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
|
||||
)
|
||||
DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
|
||||
DIAGRAM_LAYOUT = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
|
||||
)
|
||||
DIAGRAM_QUICK_STYLE = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
|
||||
)
|
||||
DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
|
||||
DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
|
||||
ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"
|
||||
EXTENDED_PROPERTIES = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
||||
)
|
||||
EXTERNAL_LINK = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink"
|
||||
)
|
||||
FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
|
||||
FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable"
|
||||
FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
||||
FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
|
||||
GLOSSARY_DOCUMENT = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument"
|
||||
)
|
||||
HANDOUT_MASTER = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster"
|
||||
)
|
||||
HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
||||
HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||
MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media"
|
||||
NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"
|
||||
NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
|
||||
NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
|
||||
OFFICE_DOCUMENT = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
)
|
||||
OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
|
||||
ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"
|
||||
PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
|
||||
PIVOT_CACHE_DEFINITION = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac"
|
||||
"heDefinition"
|
||||
)
|
||||
PIVOT_CACHE_RECORDS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh"
|
||||
"eetml/pivotCacheRecords"
|
||||
)
|
||||
PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
|
||||
PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
|
||||
PRINTER_SETTINGS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings"
|
||||
)
|
||||
QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable"
|
||||
REVISION_HEADERS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders"
|
||||
)
|
||||
REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog"
|
||||
SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
|
||||
SHARED_STRINGS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
|
||||
)
|
||||
SHEET_METADATA = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata"
|
||||
)
|
||||
SIGNATURE = (
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
|
||||
"re/signature"
|
||||
)
|
||||
SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
|
||||
SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
|
||||
SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"
|
||||
SLIDE_UPDATE_INFO = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo"
|
||||
)
|
||||
STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
|
||||
TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
|
||||
TABLE_SINGLE_CELLS = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells"
|
||||
)
|
||||
TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
|
||||
TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags"
|
||||
THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
||||
THEME_OVERRIDE = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride"
|
||||
)
|
||||
THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
|
||||
USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames"
|
||||
VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"
|
||||
VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
|
||||
VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
|
||||
VOLATILE_DEPENDENCIES = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile"
|
||||
"Dependencies"
|
||||
)
|
||||
WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings"
|
||||
WORKSHEET_SOURCE = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource"
|
||||
)
|
||||
XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps"
|
||||
@@ -0,0 +1,188 @@
|
||||
"""OPC-local oxml module to handle OPC-local concerns like relationship parsing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from pptx.opc.constants import NAMESPACE as NS
|
||||
from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
|
||||
from pptx.oxml import parse_xml, register_element_cls
|
||||
from pptx.oxml.simpletypes import (
|
||||
ST_ContentType,
|
||||
ST_Extension,
|
||||
ST_TargetMode,
|
||||
XsdAnyUri,
|
||||
XsdId,
|
||||
)
|
||||
from pptx.oxml.xmlchemy import (
|
||||
BaseOxmlElement,
|
||||
OptionalAttribute,
|
||||
RequiredAttribute,
|
||||
ZeroOrMore,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx.opc.packuri import PackURI
|
||||
|
||||
nsmap = {
|
||||
"ct": NS.OPC_CONTENT_TYPES,
|
||||
"pr": NS.OPC_RELATIONSHIPS,
|
||||
"r": NS.OFC_RELATIONSHIPS,
|
||||
}
|
||||
|
||||
|
||||
def oxml_to_encoded_bytes(
|
||||
element: BaseOxmlElement,
|
||||
encoding: str = "utf-8",
|
||||
pretty_print: bool = False,
|
||||
standalone: bool | None = None,
|
||||
) -> bytes:
|
||||
return etree.tostring(
|
||||
element, encoding=encoding, pretty_print=pretty_print, standalone=standalone
|
||||
)
|
||||
|
||||
|
||||
def oxml_tostring(
|
||||
elm: BaseOxmlElement,
|
||||
encoding: str | None = None,
|
||||
pretty_print: bool = False,
|
||||
standalone: bool | None = None,
|
||||
):
|
||||
return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone)
|
||||
|
||||
|
||||
def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes:
|
||||
"""Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file.
|
||||
|
||||
Includes XML-declaration header.
|
||||
"""
|
||||
return etree.tostring(part_elm, encoding="UTF-8", standalone=True)
|
||||
|
||||
|
||||
class CT_Default(BaseOxmlElement):
|
||||
"""`<Default>` element.
|
||||
|
||||
Specifies the default content type to be applied to a part with the specified extension.
|
||||
"""
|
||||
|
||||
extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"Extension", ST_Extension
|
||||
)
|
||||
contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"ContentType", ST_ContentType
|
||||
)
|
||||
|
||||
|
||||
class CT_Override(BaseOxmlElement):
|
||||
"""`<Override>` element.
|
||||
|
||||
Specifies the content type to be applied for a part with the specified partname.
|
||||
"""
|
||||
|
||||
partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"PartName", XsdAnyUri
|
||||
)
|
||||
contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"ContentType", ST_ContentType
|
||||
)
|
||||
|
||||
|
||||
class CT_Relationship(BaseOxmlElement):
|
||||
"""`<Relationship>` element.
|
||||
|
||||
Represents a single relationship from a source to a target part.
|
||||
"""
|
||||
|
||||
rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType]
|
||||
reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType]
|
||||
target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"Target", XsdAnyUri
|
||||
)
|
||||
targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
|
||||
"TargetMode", ST_TargetMode, default=RTM.INTERNAL
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL
|
||||
) -> CT_Relationship:
|
||||
"""Return a new `<Relationship>` element.
|
||||
|
||||
`target_ref` is either a partname or a URI.
|
||||
"""
|
||||
relationship = cast(CT_Relationship, parse_xml(f'<Relationship xmlns="{nsmap["pr"]}"/>'))
|
||||
relationship.rId = rId
|
||||
relationship.reltype = reltype
|
||||
relationship.target_ref = target_ref
|
||||
relationship.targetMode = target_mode
|
||||
return relationship
|
||||
|
||||
|
||||
class CT_Relationships(BaseOxmlElement):
|
||||
"""`<Relationships>` element, the root element in a .rels file."""
|
||||
|
||||
relationship_lst: list[CT_Relationship]
|
||||
_insert_relationship: Callable[[CT_Relationship], CT_Relationship]
|
||||
|
||||
relationship = ZeroOrMore("pr:Relationship")
|
||||
|
||||
def add_rel(
|
||||
self, rId: str, reltype: str, target: str, is_external: bool = False
|
||||
) -> CT_Relationship:
|
||||
"""Add a child `<Relationship>` element with attributes set as specified."""
|
||||
target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
|
||||
relationship = CT_Relationship.new(rId, reltype, target, target_mode)
|
||||
return self._insert_relationship(relationship)
|
||||
|
||||
@classmethod
|
||||
def new(cls) -> CT_Relationships:
|
||||
"""Return a new `<Relationships>` element."""
|
||||
return cast(CT_Relationships, parse_xml(f'<Relationships xmlns="{nsmap["pr"]}"/>'))
|
||||
|
||||
@property
|
||||
def xml_file_bytes(self) -> bytes:
|
||||
"""Return XML bytes, with XML-declaration, for this `<Relationships>` element.
|
||||
|
||||
Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at
|
||||
the top.
|
||||
"""
|
||||
return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True)
|
||||
|
||||
|
||||
class CT_Types(BaseOxmlElement):
|
||||
"""`<Types>` element.
|
||||
|
||||
The container element for Default and Override elements in [Content_Types].xml.
|
||||
"""
|
||||
|
||||
default_lst: list[CT_Default]
|
||||
override_lst: list[CT_Override]
|
||||
|
||||
_add_default: Callable[..., CT_Default]
|
||||
_add_override: Callable[..., CT_Override]
|
||||
|
||||
default = ZeroOrMore("ct:Default")
|
||||
override = ZeroOrMore("ct:Override")
|
||||
|
||||
def add_default(self, ext: str, content_type: str) -> CT_Default:
|
||||
"""Add a child `<Default>` element with attributes set to parameter values."""
|
||||
return self._add_default(extension=ext, contentType=content_type)
|
||||
|
||||
def add_override(self, partname: PackURI, content_type: str) -> CT_Override:
|
||||
"""Add a child `<Override>` element with attributes set to parameter values."""
|
||||
return self._add_override(partName=partname, contentType=content_type)
|
||||
|
||||
@classmethod
|
||||
def new(cls) -> CT_Types:
|
||||
"""Return a new `<Types>` element."""
|
||||
return cast(CT_Types, parse_xml(f'<Types xmlns="{nsmap["ct"]}"/>'))
|
||||
|
||||
|
||||
register_element_cls("ct:Default", CT_Default)
|
||||
register_element_cls("ct:Override", CT_Override)
|
||||
register_element_cls("ct:Types", CT_Types)
|
||||
|
||||
register_element_cls("pr:Relationship", CT_Relationship)
|
||||
register_element_cls("pr:Relationships", CT_Relationships)
|
||||
@@ -0,0 +1,762 @@
|
||||
"""Fundamental Open Packaging Convention (OPC) objects.
|
||||
|
||||
The :mod:`pptx.packaging` module coheres around the concerns of reading and writing
|
||||
presentations to and from a .pptx file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
|
||||
|
||||
from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
|
||||
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
|
||||
from pptx.opc.oxml import CT_Relationships, serialize_part_xml
|
||||
from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
|
||||
from pptx.opc.serialized import PackageReader, PackageWriter
|
||||
from pptx.opc.shared import CaseInsensitiveDict
|
||||
from pptx.oxml import parse_xml
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from pptx.opc.oxml import CT_Relationship, CT_Types
|
||||
from pptx.oxml.xmlchemy import BaseOxmlElement
|
||||
from pptx.package import Package
|
||||
from pptx.parts.presentation import PresentationPart
|
||||
|
||||
|
||||
class _RelatableMixin:
|
||||
"""Provide relationship methods required by both the package and each part."""
|
||||
|
||||
def part_related_by(self, reltype: str) -> Part:
|
||||
"""Return (single) part having relationship to this package of `reltype`.
|
||||
|
||||
Raises |KeyError| if no such relationship is found and |ValueError| if more than one such
|
||||
relationship is found.
|
||||
"""
|
||||
return self._rels.part_with_reltype(reltype)
|
||||
|
||||
def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str:
|
||||
"""Return rId key of relationship of `reltype` to `target`.
|
||||
|
||||
If such a relationship already exists, its rId is returned. Otherwise the relationship is
|
||||
added and its new rId returned.
|
||||
"""
|
||||
if isinstance(target, str):
|
||||
assert is_external
|
||||
return self._rels.get_or_add_ext_rel(reltype, target)
|
||||
|
||||
return self._rels.get_or_add(reltype, target)
|
||||
|
||||
def related_part(self, rId: str) -> Part:
|
||||
"""Return related |Part| subtype identified by `rId`."""
|
||||
return self._rels[rId].target_part
|
||||
|
||||
def target_ref(self, rId: str) -> str:
|
||||
"""Return URL contained in target ref of relationship identified by `rId`."""
|
||||
return self._rels[rId].target_ref
|
||||
|
||||
@lazyproperty
|
||||
def _rels(self) -> _Relationships:
|
||||
"""|_Relationships| object containing relationships from this part to others."""
|
||||
raise NotImplementedError( # pragma: no cover
|
||||
"`%s` must implement `.rels`" % type(self).__name__
|
||||
)
|
||||
|
||||
|
||||
class OpcPackage(_RelatableMixin):
|
||||
"""Main API class for |python-opc|.
|
||||
|
||||
A new instance is constructed by calling the :meth:`open` classmethod with a path to a package
|
||||
file or file-like object containing a package (.pptx file).
|
||||
"""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes]):
|
||||
self._pkg_file = pkg_file
|
||||
|
||||
@classmethod
|
||||
def open(cls, pkg_file: str | IO[bytes]) -> Self:
|
||||
"""Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
|
||||
return cls(pkg_file)._load()
|
||||
|
||||
def drop_rel(self, rId: str) -> None:
|
||||
"""Remove relationship identified by `rId`."""
|
||||
self._rels.pop(rId)
|
||||
|
||||
def iter_parts(self) -> Iterator[Part]:
|
||||
"""Generate exactly one reference to each part in the package."""
|
||||
visited: Set[Part] = set()
|
||||
for rel in self.iter_rels():
|
||||
if rel.is_external:
|
||||
continue
|
||||
part = rel.target_part
|
||||
if part in visited:
|
||||
continue
|
||||
yield part
|
||||
visited.add(part)
|
||||
|
||||
def iter_rels(self) -> Iterator[_Relationship]:
|
||||
"""Generate exactly one reference to each relationship in package.
|
||||
|
||||
Performs a depth-first traversal of the rels graph.
|
||||
"""
|
||||
visited: Set[Part] = set()
|
||||
|
||||
def walk_rels(rels: _Relationships) -> Iterator[_Relationship]:
|
||||
for rel in rels.values():
|
||||
yield rel
|
||||
# --- external items can have no relationships ---
|
||||
if rel.is_external:
|
||||
continue
|
||||
# -- all relationships other than those for the package belong to a part. Once
|
||||
# -- that part has been processed, processing it again would lead to the same
|
||||
# -- relationships appearing more than once.
|
||||
part = rel.target_part
|
||||
if part in visited:
|
||||
continue
|
||||
visited.add(part)
|
||||
# --- recurse into relationships of each unvisited target-part ---
|
||||
yield from walk_rels(part.rels)
|
||||
|
||||
yield from walk_rels(self._rels)
|
||||
|
||||
@property
|
||||
def main_document_part(self) -> PresentationPart:
|
||||
"""Return |Part| subtype serving as the main document part for this package.
|
||||
|
||||
In this case it will be a |Presentation| part.
|
||||
"""
|
||||
return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT))
|
||||
|
||||
def next_partname(self, tmpl: str) -> PackURI:
|
||||
"""Return |PackURI| next available partname matching `tmpl`.
|
||||
|
||||
`tmpl` is a printf (%)-style template string containing a single replacement item, a '%d'
|
||||
to be used to insert the integer portion of the partname. Example:
|
||||
'/ppt/slides/slide%d.xml'
|
||||
"""
|
||||
# --- expected next partname is tmpl % n where n is one greater than the number
|
||||
# --- of existing partnames that match tmpl. Speed up finding the next one
|
||||
# --- (maybe) by searching from the end downward rather than from 1 upward.
|
||||
prefix = tmpl[: (tmpl % 42).find("42")]
|
||||
partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)}
|
||||
for n in range(len(partnames) + 1, 0, -1):
|
||||
candidate_partname = tmpl % n
|
||||
if candidate_partname not in partnames:
|
||||
return PackURI(candidate_partname)
|
||||
raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover
|
||||
|
||||
def save(self, pkg_file: str | IO[bytes]) -> None:
|
||||
"""Save this package to `pkg_file`.
|
||||
|
||||
`file` can be either a path to a file (a string) or a file-like object.
|
||||
"""
|
||||
PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
|
||||
|
||||
def _load(self) -> Self:
|
||||
"""Return the package after loading all parts and relationships."""
|
||||
pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self))
|
||||
self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts)
|
||||
return self
|
||||
|
||||
@lazyproperty
|
||||
def _rels(self) -> _Relationships:
|
||||
"""|Relationships| object containing relationships of this package."""
|
||||
return _Relationships(PACKAGE_URI.baseURI)
|
||||
|
||||
|
||||
class _PackageLoader:
|
||||
"""Function-object that loads a package from disk (or other store)."""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes], package: Package):
|
||||
self._pkg_file = pkg_file
|
||||
self._package = package
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls, pkg_file: str | IO[bytes], package: Package
|
||||
) -> tuple[CT_Relationships, dict[PackURI, Part]]:
|
||||
"""Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
|
||||
|
||||
The returned `parts` value is a {partname: part} mapping with each part in the package
|
||||
included and constructed complete with its relationships to other parts in the package.
|
||||
|
||||
The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed
|
||||
package relationships. It is the caller's responsibility (the package object) to load
|
||||
those relationships into its |_Relationships| object.
|
||||
"""
|
||||
return cls(pkg_file, package)._load()
|
||||
|
||||
def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]:
|
||||
"""Return (pkg_xml_rels, parts) pair resulting from loading pkg_file."""
|
||||
parts, xml_rels = self._parts, self._xml_rels
|
||||
|
||||
for partname, part in parts.items():
|
||||
part.load_rels_from_xml(xml_rels[partname], parts)
|
||||
|
||||
return xml_rels[PACKAGE_URI], parts
|
||||
|
||||
@lazyproperty
|
||||
def _content_types(self) -> _ContentTypeMap:
|
||||
"""|_ContentTypeMap| object providing content-types for items of this package.
|
||||
|
||||
Provides a content-type (MIME-type) for any given partname.
|
||||
"""
|
||||
return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI])
|
||||
|
||||
@lazyproperty
|
||||
def _package_reader(self) -> PackageReader:
|
||||
"""|PackageReader| object providing access to package-items in pkg_file."""
|
||||
return PackageReader(self._pkg_file)
|
||||
|
||||
@lazyproperty
|
||||
def _parts(self) -> dict[PackURI, Part]:
|
||||
"""dict {partname: Part} populated with parts loading from package.
|
||||
|
||||
Among other duties, this collection is passed to each relationships collection so each
|
||||
relationship can resolve a reference to its target part when required. This reference can
|
||||
only be reliably carried out once the all parts have been loaded.
|
||||
"""
|
||||
content_types = self._content_types
|
||||
package = self._package
|
||||
package_reader = self._package_reader
|
||||
|
||||
return {
|
||||
partname: PartFactory(
|
||||
partname,
|
||||
content_types[partname],
|
||||
package,
|
||||
blob=package_reader[partname],
|
||||
)
|
||||
for partname in (p for p in self._xml_rels if p != "/")
|
||||
# -- invalid partnames can arise in some packages; ignore those rather than raise an
|
||||
# -- exception.
|
||||
if partname in package_reader
|
||||
}
|
||||
|
||||
@lazyproperty
|
||||
def _xml_rels(self) -> dict[PackURI, CT_Relationships]:
|
||||
"""dict {partname: xml_rels} for package and all package parts.
|
||||
|
||||
This is used as the basis for other loading operations such as loading parts and
|
||||
populating their relationships.
|
||||
"""
|
||||
xml_rels: dict[PackURI, CT_Relationships] = {}
|
||||
visited_partnames: Set[PackURI] = set()
|
||||
|
||||
def load_rels(source_partname: PackURI, rels: CT_Relationships):
|
||||
"""Populate `xml_rels` dict by traversing relationships depth-first."""
|
||||
xml_rels[source_partname] = rels
|
||||
visited_partnames.add(source_partname)
|
||||
base_uri = source_partname.baseURI
|
||||
|
||||
# --- recursion stops when there are no unvisited partnames in rels ---
|
||||
for rel in rels.relationship_lst:
|
||||
if rel.targetMode == RTM.EXTERNAL:
|
||||
continue
|
||||
target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref)
|
||||
if target_partname in visited_partnames:
|
||||
continue
|
||||
load_rels(target_partname, self._xml_rels_for(target_partname))
|
||||
|
||||
load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI))
|
||||
return xml_rels
|
||||
|
||||
def _xml_rels_for(self, partname: PackURI) -> CT_Relationships:
|
||||
"""Return CT_Relationships object formed by parsing rels XML for `partname`.
|
||||
|
||||
A CT_Relationships object is returned in all cases. A part that has no relationships
|
||||
receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects.
|
||||
"""
|
||||
rels_xml = self._package_reader.rels_xml_for(partname)
|
||||
return (
|
||||
CT_Relationships.new()
|
||||
if rels_xml is None
|
||||
else cast(CT_Relationships, parse_xml(rels_xml))
|
||||
)
|
||||
|
||||
|
||||
class Part(_RelatableMixin):
|
||||
"""Base class for package parts.
|
||||
|
||||
Provides common properties and methods, but intended to be subclassed in client code to
|
||||
implement specific part behaviors. Also serves as the default class for parts that are not yet
|
||||
given specific behaviors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None
|
||||
):
|
||||
# --- XmlPart subtypes, don't store a blob (the original XML) ---
|
||||
self._partname = partname
|
||||
self._content_type = content_type
|
||||
self._package = package
|
||||
self._blob = blob
|
||||
|
||||
@classmethod
|
||||
def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self:
|
||||
"""Return `cls` instance loaded from arguments.
|
||||
|
||||
This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart
|
||||
for an example.
|
||||
"""
|
||||
return cls(partname, content_type, package, blob)
|
||||
|
||||
@property
|
||||
def blob(self) -> bytes:
|
||||
"""Contents of this package part as a sequence of bytes.
|
||||
|
||||
Intended to be overridden by subclasses. Default behavior is to return the blob initial
|
||||
loaded during `Package.open()` operation.
|
||||
"""
|
||||
return self._blob or b""
|
||||
|
||||
@blob.setter
|
||||
def blob(self, blob: bytes):
|
||||
"""Note that not all subclasses use the part blob as their blob source.
|
||||
|
||||
In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on
|
||||
demand. This works fine for binary parts though.
|
||||
"""
|
||||
self._blob = blob
|
||||
|
||||
@lazyproperty
|
||||
def content_type(self) -> str:
|
||||
"""Content-type (MIME-type) of this part."""
|
||||
return self._content_type
|
||||
|
||||
def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None:
|
||||
"""load _Relationships for this part from `xml_rels`.
|
||||
|
||||
Part references are resolved using the `parts` dict that maps each partname to the loaded
|
||||
part with that partname. These relationships are loaded from a serialized package and so
|
||||
already have assigned rIds. This method is only used during package loading.
|
||||
"""
|
||||
self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts)
|
||||
|
||||
@lazyproperty
|
||||
def package(self) -> Package:
|
||||
"""Package this part belongs to."""
|
||||
return self._package
|
||||
|
||||
@property
|
||||
def partname(self) -> PackURI:
|
||||
"""|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml"."""
|
||||
return self._partname
|
||||
|
||||
@partname.setter
|
||||
def partname(self, partname: PackURI):
|
||||
if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
|
||||
raise TypeError( # pragma: no cover
|
||||
"partname must be instance of PackURI, got '%s'" % type(partname).__name__
|
||||
)
|
||||
self._partname = partname
|
||||
|
||||
@lazyproperty
|
||||
def rels(self) -> _Relationships:
|
||||
"""Collection of relationships from this part to other parts."""
|
||||
# --- this must be public to allow the part graph to be traversed ---
|
||||
return self._rels
|
||||
|
||||
def _blob_from_file(self, file: str | IO[bytes]) -> bytes:
|
||||
"""Return bytes of `file`, which is either a str path or a file-like object."""
|
||||
# --- a str `file` is assumed to be a path ---
|
||||
if isinstance(file, str):
|
||||
with open(file, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
# --- otherwise, assume `file` is a file-like object
|
||||
# --- reposition file cursor if it has one
|
||||
if callable(getattr(file, "seek")):
|
||||
file.seek(0)
|
||||
return file.read()
|
||||
|
||||
@lazyproperty
|
||||
def _rels(self) -> _Relationships:
|
||||
"""Relationships from this part to others."""
|
||||
return _Relationships(self._partname.baseURI)
|
||||
|
||||
|
||||
class XmlPart(Part):
|
||||
"""Base class for package parts containing an XML payload, which is most of them.
|
||||
|
||||
Provides additional methods to the |Part| base class that take care of parsing and
|
||||
reserializing the XML payload and managing relationships to other parts.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement
|
||||
):
|
||||
super(XmlPart, self).__init__(partname, content_type, package)
|
||||
self._element = element
|
||||
|
||||
@classmethod
|
||||
def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes):
|
||||
"""Return instance of `cls` loaded with parsed XML from `blob`."""
|
||||
return cls(
|
||||
partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob))
|
||||
)
|
||||
|
||||
@property
|
||||
def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""bytes XML serialization of this part."""
|
||||
return serialize_part_xml(self._element)
|
||||
|
||||
# -- XmlPart cannot set its blob, which is why pyright complains --
|
||||
|
||||
def drop_rel(self, rId: str) -> None:
|
||||
"""Remove relationship identified by `rId` if its reference count is under 2.
|
||||
|
||||
Relationships with a reference count of 0 are implicit relationships. Note that only XML
|
||||
parts can drop relationships.
|
||||
"""
|
||||
if self._rel_ref_count(rId) < 2:
|
||||
self._rels.pop(rId)
|
||||
|
||||
@property
|
||||
def part(self):
|
||||
"""This part.
|
||||
|
||||
This is part of the parent protocol, "children" of the document will not know the part
|
||||
that contains them so must ask their parent object. That chain of delegation ends here for
|
||||
child objects.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _rel_ref_count(self, rId: str) -> int:
|
||||
"""Return int count of references in this part's XML to `rId`."""
|
||||
return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId])
|
||||
|
||||
|
||||
class PartFactory:
|
||||
"""Constructs a registered subtype of |Part|.
|
||||
|
||||
Client code can register a subclass of |Part| to be used for a package blob based on its
|
||||
content type.
|
||||
"""
|
||||
|
||||
part_type_for: dict[str, type[Part]] = {}
|
||||
|
||||
def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part:
|
||||
PartClass = cls._part_cls_for(content_type)
|
||||
return PartClass.load(partname, content_type, package, blob)
|
||||
|
||||
@classmethod
|
||||
def _part_cls_for(cls, content_type: str) -> type[Part]:
|
||||
"""Return the custom part class registered for `content_type`.
|
||||
|
||||
Returns |Part| if no custom class is registered for `content_type`.
|
||||
"""
|
||||
if content_type in cls.part_type_for:
|
||||
return cls.part_type_for[content_type]
|
||||
return Part
|
||||
|
||||
|
||||
class _ContentTypeMap:
|
||||
"""Value type providing dict semantics for looking up content type by partname."""
|
||||
|
||||
def __init__(self, overrides: dict[str, str], defaults: dict[str, str]):
|
||||
self._overrides = overrides
|
||||
self._defaults = defaults
|
||||
|
||||
def __getitem__(self, partname: PackURI) -> str:
|
||||
"""Return content-type (MIME-type) for part identified by *partname*."""
|
||||
if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
|
||||
raise TypeError(
|
||||
"_ContentTypeMap key must be <type 'PackURI'>, got %s" % type(partname).__name__
|
||||
)
|
||||
|
||||
if partname in self._overrides:
|
||||
return self._overrides[partname]
|
||||
|
||||
if partname.ext in self._defaults:
|
||||
return self._defaults[partname.ext]
|
||||
|
||||
raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap:
|
||||
"""Return |_ContentTypeMap| instance populated from `content_types_xml`."""
|
||||
types_elm = cast("CT_Types", parse_xml(content_types_xml))
|
||||
# -- note all partnames in [Content_Types].xml are absolute --
|
||||
overrides = CaseInsensitiveDict(
|
||||
(o.partName.lower(), o.contentType) for o in types_elm.override_lst
|
||||
)
|
||||
defaults = CaseInsensitiveDict(
|
||||
(d.extension.lower(), d.contentType) for d in types_elm.default_lst
|
||||
)
|
||||
return cls(overrides, defaults)
|
||||
|
||||
|
||||
class _Relationships(Mapping[str, "_Relationship"]):
|
||||
"""Collection of |_Relationship| instances having `dict` semantics.
|
||||
|
||||
Relationships are keyed by their rId, but may also be found in other ways, such as by their
|
||||
relationship type. |Relationship| objects are keyed by their rId.
|
||||
|
||||
Iterating this collection has normal mapping semantics, generating the keys (rIds) of the
|
||||
mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a
|
||||
`dict`.
|
||||
"""
|
||||
|
||||
def __init__(self, base_uri: str):
|
||||
self._base_uri = base_uri
|
||||
|
||||
def __contains__(self, rId: object) -> bool:
|
||||
"""Implement 'in' operation, like `"rId7" in relationships`."""
|
||||
return rId in self._rels
|
||||
|
||||
def __getitem__(self, rId: str) -> _Relationship:
|
||||
"""Implement relationship lookup by rId using indexed access, like rels[rId]."""
|
||||
try:
|
||||
return self._rels[rId]
|
||||
except KeyError:
|
||||
raise KeyError("no relationship with key '%s'" % rId)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
"""Implement iteration of rIds (iterating a mapping produces its keys)."""
|
||||
return iter(self._rels)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return count of relationships in collection."""
|
||||
return len(self._rels)
|
||||
|
||||
def get_or_add(self, reltype: str, target_part: Part) -> str:
|
||||
"""Return str rId of `reltype` to `target_part`.
|
||||
|
||||
The rId of an existing matching relationship is used if present. Otherwise, a new
|
||||
relationship is added and that rId is returned.
|
||||
"""
|
||||
existing_rId = self._get_matching(reltype, target_part)
|
||||
return (
|
||||
self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId
|
||||
)
|
||||
|
||||
def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str:
|
||||
"""Return str rId of external relationship of `reltype` to `target_ref`.
|
||||
|
||||
The rId of an existing matching relationship is used if present. Otherwise, a new
|
||||
relationship is added and that rId is returned.
|
||||
"""
|
||||
existing_rId = self._get_matching(reltype, target_ref, is_external=True)
|
||||
return (
|
||||
self._add_relationship(reltype, target_ref, is_external=True)
|
||||
if existing_rId is None
|
||||
else existing_rId
|
||||
)
|
||||
|
||||
def load_from_xml(
|
||||
self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part]
|
||||
) -> None:
|
||||
"""Replace any relationships in this collection with those from `xml_rels`."""
|
||||
|
||||
def iter_valid_rels():
|
||||
"""Filter out broken relationships such as those pointing to NULL."""
|
||||
for rel_elm in xml_rels.relationship_lst:
|
||||
# --- Occasionally a PowerPoint plugin or other client will "remove"
|
||||
# --- a relationship simply by "voiding" its Target value, like making
|
||||
# --- it "/ppt/slides/NULL". Skip any relationships linking to a
|
||||
# --- partname that is not present in the package.
|
||||
if rel_elm.targetMode == RTM.INTERNAL:
|
||||
partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref)
|
||||
if partname not in parts:
|
||||
continue
|
||||
yield _Relationship.from_xml(base_uri, rel_elm, parts)
|
||||
|
||||
self._rels.clear()
|
||||
self._rels.update((rel.rId, rel) for rel in iter_valid_rels())
|
||||
|
||||
def part_with_reltype(self, reltype: str) -> Part:
|
||||
"""Return target part of relationship with matching `reltype`.
|
||||
|
||||
Raises |KeyError| if not found and |ValueError| if more than one matching relationship is
|
||||
found.
|
||||
"""
|
||||
rels_of_reltype = self._rels_by_reltype[reltype]
|
||||
|
||||
if len(rels_of_reltype) == 0:
|
||||
raise KeyError("no relationship of type '%s' in collection" % reltype)
|
||||
|
||||
if len(rels_of_reltype) > 1:
|
||||
raise ValueError("multiple relationships of type '%s' in collection" % reltype)
|
||||
|
||||
return rels_of_reltype[0].target_part
|
||||
|
||||
def pop(self, rId: str) -> _Relationship:
|
||||
"""Return |_Relationship| identified by `rId` after removing it from collection.
|
||||
|
||||
The caller is responsible for ensuring it is no longer required.
|
||||
"""
|
||||
return self._rels.pop(rId)
|
||||
|
||||
@property
|
||||
def xml(self):
|
||||
"""bytes XML serialization of this relationship collection.
|
||||
|
||||
This value is suitable for storage as a .rels file in an OPC package. Includes a `<?xml..`
|
||||
declaration header with encoding as UTF-8.
|
||||
"""
|
||||
rels_elm = CT_Relationships.new()
|
||||
|
||||
# -- Sequence <Relationship> elements deterministically (in numerical order) to
|
||||
# -- simplify testing and manual inspection.
|
||||
def iter_rels_in_numerical_order():
|
||||
sorted_num_rId_pairs = sorted(
|
||||
(
|
||||
int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0,
|
||||
rId,
|
||||
)
|
||||
for rId in self.keys()
|
||||
)
|
||||
return (self[rId] for _, rId in sorted_num_rId_pairs)
|
||||
|
||||
for rel in iter_rels_in_numerical_order():
|
||||
rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external)
|
||||
|
||||
return rels_elm.xml_file_bytes
|
||||
|
||||
def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str:
|
||||
"""Return str rId of |_Relationship| newly added to spec."""
|
||||
rId = self._next_rId
|
||||
self._rels[rId] = _Relationship(
|
||||
self._base_uri,
|
||||
rId,
|
||||
reltype,
|
||||
target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL,
|
||||
target=target,
|
||||
)
|
||||
return rId
|
||||
|
||||
def _get_matching(
|
||||
self, reltype: str, target: Part | str, is_external: bool = False
|
||||
) -> str | None:
|
||||
"""Return optional str rId of rel of `reltype`, `target`, and `is_external`.
|
||||
|
||||
Returns `None` on no matching relationship
|
||||
"""
|
||||
for rel in self._rels_by_reltype[reltype]:
|
||||
if rel.is_external != is_external:
|
||||
continue
|
||||
rel_target = rel.target_ref if rel.is_external else rel.target_part
|
||||
if rel_target == target:
|
||||
return rel.rId
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def _next_rId(self) -> str:
|
||||
"""Next str rId available in collection.
|
||||
|
||||
The next rId is the first unused key starting from "rId1" and making use of any gaps in
|
||||
numbering, e.g. 'rId2' for rIds ['rId1', 'rId3'].
|
||||
"""
|
||||
# --- The common case is where all sequential numbers starting at "rId1" are
|
||||
# --- used and the next available rId is "rId%d" % (len(rels)+1). So we start
|
||||
# --- there and count down to produce the best performance.
|
||||
for n in range(len(self) + 1, 0, -1):
|
||||
rId_candidate = "rId%d" % n # like 'rId19'
|
||||
if rId_candidate not in self._rels:
|
||||
return rId_candidate
|
||||
raise Exception(
|
||||
"ProgrammingError: Impossible to have more distinct rIds than relationships"
|
||||
)
|
||||
|
||||
@lazyproperty
|
||||
def _rels(self) -> dict[str, _Relationship]:
|
||||
"""dict {rId: _Relationship} containing relationships of this collection."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def _rels_by_reltype(self) -> dict[str, list[_Relationship]]:
|
||||
"""defaultdict {reltype: [rels]} for all relationships in collection."""
|
||||
D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list)
|
||||
for rel in self.values():
|
||||
D[rel.reltype].append(rel)
|
||||
return D
|
||||
|
||||
|
||||
class _Relationship:
|
||||
"""Value object describing link from a part or package to another part."""
|
||||
|
||||
def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str):
|
||||
self._base_uri = base_uri
|
||||
self._rId = rId
|
||||
self._reltype = reltype
|
||||
self._target_mode = target_mode
|
||||
self._target = target
|
||||
|
||||
@classmethod
|
||||
def from_xml(
|
||||
cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part]
|
||||
) -> _Relationship:
|
||||
"""Return |_Relationship| object based on CT_Relationship element `rel`."""
|
||||
target = (
|
||||
rel.target_ref
|
||||
if rel.targetMode == RTM.EXTERNAL
|
||||
else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)]
|
||||
)
|
||||
return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target)
|
||||
|
||||
@lazyproperty
|
||||
def is_external(self) -> bool:
|
||||
"""True if target_mode is `RTM.EXTERNAL`.
|
||||
|
||||
An external relationship is a link to a resource outside the package, such as a
|
||||
web-resource (URL).
|
||||
"""
|
||||
return self._target_mode == RTM.EXTERNAL
|
||||
|
||||
@lazyproperty
|
||||
def reltype(self) -> str:
|
||||
"""Member of RELATIONSHIP_TYPE describing relationship of target to source."""
|
||||
return self._reltype
|
||||
|
||||
@lazyproperty
|
||||
def rId(self) -> str:
|
||||
"""str relationship-id, like 'rId9'.
|
||||
|
||||
Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies
|
||||
this relationship within its peers for the source-part or package.
|
||||
"""
|
||||
return self._rId
|
||||
|
||||
@lazyproperty
|
||||
def target_part(self) -> Part:
|
||||
"""|Part| or subtype referred to by this relationship."""
|
||||
if self.is_external:
|
||||
raise ValueError(
|
||||
"`.target_part` property on _Relationship is undefined when "
|
||||
"target-mode is external"
|
||||
)
|
||||
assert isinstance(self._target, Part)
|
||||
return self._target
|
||||
|
||||
@lazyproperty
|
||||
def target_partname(self) -> PackURI:
|
||||
"""|PackURI| instance containing partname targeted by this relationship.
|
||||
|
||||
Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to
|
||||
check before referencing.
|
||||
"""
|
||||
if self.is_external:
|
||||
raise ValueError(
|
||||
"`.target_partname` property on _Relationship is undefined when "
|
||||
"target-mode is external"
|
||||
)
|
||||
assert isinstance(self._target, Part)
|
||||
return self._target.partname
|
||||
|
||||
@lazyproperty
|
||||
def target_ref(self) -> str:
|
||||
"""str reference to relationship target.
|
||||
|
||||
For internal relationships this is the relative partname, suitable for serialization
|
||||
purposes. For an external relationship it is typically a URL.
|
||||
"""
|
||||
if self.is_external:
|
||||
assert isinstance(self._target, str)
|
||||
return self._target
|
||||
|
||||
return self.target_partname.relative_ref(self._base_uri)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import posixpath
|
||||
import re
|
||||
|
||||
|
||||
class PackURI(str):
|
||||
"""Proxy for a pack URI (partname).
|
||||
|
||||
Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise.
|
||||
"""
|
||||
|
||||
_filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?")
|
||||
|
||||
def __new__(cls, pack_uri_str: str):
|
||||
if not pack_uri_str[0] == "/":
|
||||
raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}")
|
||||
return str.__new__(cls, pack_uri_str)
|
||||
|
||||
@staticmethod
|
||||
def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI:
|
||||
"""Construct an absolute pack URI formed by translating `relative_ref` onto `baseURI`."""
|
||||
joined_uri = posixpath.join(baseURI, relative_ref)
|
||||
abs_uri = posixpath.abspath(joined_uri)
|
||||
return PackURI(abs_uri)
|
||||
|
||||
@property
|
||||
def baseURI(self) -> str:
|
||||
"""The base URI of this pack URI; the directory portion, roughly speaking.
|
||||
|
||||
E.g. `"/ppt/slides"` for `"/ppt/slides/slide1.xml"`.
|
||||
|
||||
For the package pseudo-partname "/", the baseURI is "/".
|
||||
"""
|
||||
return posixpath.split(self)[0]
|
||||
|
||||
@property
|
||||
def ext(self) -> str:
|
||||
"""The extension portion of this pack URI.
|
||||
|
||||
E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included.
|
||||
"""
|
||||
# -- raw_ext is either empty string or starts with period, e.g. ".xml" --
|
||||
raw_ext = posixpath.splitext(self)[1]
|
||||
return raw_ext[1:] if raw_ext.startswith(".") else raw_ext
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""The "filename" portion of this pack URI.
|
||||
|
||||
E.g. `"slide1.xml"` for `"/ppt/slides/slide1.xml"`.
|
||||
|
||||
For the package pseudo-partname "/", `filename` is ''.
|
||||
"""
|
||||
return posixpath.split(self)[1]
|
||||
|
||||
@property
|
||||
def idx(self) -> int | None:
|
||||
"""Optional int partname index.
|
||||
|
||||
Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for
|
||||
`"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`.
|
||||
"""
|
||||
filename = self.filename
|
||||
if not filename:
|
||||
return None
|
||||
name_part = posixpath.splitext(filename)[0] # filename w/ext removed
|
||||
match = self._filename_re.match(name_part)
|
||||
if match is None:
|
||||
return None
|
||||
if match.group(2):
|
||||
return int(match.group(2))
|
||||
return None
|
||||
|
||||
@property
|
||||
def membername(self) -> str:
|
||||
"""The pack URI with the leading slash stripped off.
|
||||
|
||||
This is the form used as the Zip file membername for the package item. Returns "" for the
|
||||
package pseudo-partname "/".
|
||||
"""
|
||||
return self[1:]
|
||||
|
||||
def relative_ref(self, baseURI: str) -> str:
|
||||
"""Return string containing relative reference to package item from `baseURI`.
|
||||
|
||||
E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return
|
||||
"../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides".
|
||||
"""
|
||||
# workaround for posixpath bug in 2.6, doesn't generate correct
|
||||
# relative path when `start` (second) parameter is root ("/")
|
||||
return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI)
|
||||
|
||||
@property
|
||||
def rels_uri(self) -> PackURI:
|
||||
"""The pack URI of the .rels part corresponding to the current pack URI.
|
||||
|
||||
Only produces sensible output if the pack URI is a partname or the package pseudo-partname
|
||||
"/".
|
||||
"""
|
||||
rels_filename = "%s.rels" % self.filename
|
||||
rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename)
|
||||
return PackURI(rels_uri_str)
|
||||
|
||||
|
||||
PACKAGE_URI = PackURI("/")
|
||||
CONTENT_TYPES_URI = PackURI("/[Content_Types].xml")
|
||||
@@ -0,0 +1,296 @@
|
||||
"""API for reading/writing serialized Open Packaging Convention (OPC) package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import zipfile
|
||||
from typing import IO, TYPE_CHECKING, Any, Container, Sequence
|
||||
|
||||
from pptx.exc import PackageNotFoundError
|
||||
from pptx.opc.constants import CONTENT_TYPE as CT
|
||||
from pptx.opc.oxml import CT_Types, serialize_part_xml
|
||||
from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
|
||||
from pptx.opc.shared import CaseInsensitiveDict
|
||||
from pptx.opc.spec import default_content_types
|
||||
from pptx.util import lazyproperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pptx.opc.package import Part, _Relationships # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
|
||||
class PackageReader(Container[bytes]):
|
||||
"""Provides access to package-parts of an OPC package with dict semantics.
|
||||
|
||||
The package may be in zip-format (a .pptx file) or expanded into a directory structure,
|
||||
perhaps by unzipping a .pptx file.
|
||||
"""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes]):
|
||||
self._pkg_file = pkg_file
|
||||
|
||||
def __contains__(self, pack_uri: object) -> bool:
|
||||
"""Return True when part identified by `pack_uri` is present in package."""
|
||||
return pack_uri in self._blob_reader
|
||||
|
||||
def __getitem__(self, pack_uri: PackURI) -> bytes:
|
||||
"""Return bytes for part corresponding to `pack_uri`."""
|
||||
return self._blob_reader[pack_uri]
|
||||
|
||||
def rels_xml_for(self, partname: PackURI) -> bytes | None:
|
||||
"""Return optional rels item XML for `partname`.
|
||||
|
||||
Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI|
|
||||
instance.
|
||||
"""
|
||||
blob_reader, uri = self._blob_reader, partname.rels_uri
|
||||
return blob_reader[uri] if uri in blob_reader else None
|
||||
|
||||
@lazyproperty
|
||||
def _blob_reader(self) -> _PhysPkgReader:
|
||||
"""|_PhysPkgReader| subtype providing read access to the package file."""
|
||||
return _PhysPkgReader.factory(self._pkg_file)
|
||||
|
||||
|
||||
class PackageWriter:
|
||||
"""Writes a zip-format OPC package to `pkg_file`.
|
||||
|
||||
`pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is
|
||||
the |_Relationships| object containing relationships for the package. `parts` is a sequence of
|
||||
|Part| subtype instance to be written to the package.
|
||||
|
||||
Its single API classmethod is :meth:`write`. This class is not intended to be instantiated.
|
||||
"""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]):
|
||||
self._pkg_file = pkg_file
|
||||
self._pkg_rels = pkg_rels
|
||||
self._parts = parts
|
||||
|
||||
@classmethod
|
||||
def write(
|
||||
cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]
|
||||
) -> None:
|
||||
"""Write a physical package (.pptx file) to `pkg_file`.
|
||||
|
||||
The serialized package contains `pkg_rels` and `parts`, a content-types stream based on
|
||||
the content type of each part, and a .rels file for each part that has relationships.
|
||||
"""
|
||||
cls(pkg_file, pkg_rels, parts)._write()
|
||||
|
||||
def _write(self) -> None:
|
||||
"""Write physical package (.pptx file)."""
|
||||
with _PhysPkgWriter.factory(self._pkg_file) as phys_writer:
|
||||
self._write_content_types_stream(phys_writer)
|
||||
self._write_pkg_rels(phys_writer)
|
||||
self._write_parts(phys_writer)
|
||||
|
||||
def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None:
|
||||
"""Write `[Content_Types].xml` part to the physical package.
|
||||
|
||||
This part must contain an appropriate content type lookup target for each part in the
|
||||
package.
|
||||
"""
|
||||
phys_writer.write(
|
||||
CONTENT_TYPES_URI,
|
||||
serialize_part_xml(_ContentTypesItem.xml_for(self._parts)),
|
||||
)
|
||||
|
||||
def _write_parts(self, phys_writer: _PhysPkgWriter) -> None:
|
||||
"""Write blob of each part in `parts` to the package.
|
||||
|
||||
A rels item for each part is also written when the part has relationships.
|
||||
"""
|
||||
for part in self._parts:
|
||||
phys_writer.write(part.partname, part.blob)
|
||||
if part._rels: # pyright: ignore[reportPrivateUsage]
|
||||
phys_writer.write(part.partname.rels_uri, part.rels.xml)
|
||||
|
||||
def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None:
|
||||
"""Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package."""
|
||||
phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml)
|
||||
|
||||
|
||||
class _PhysPkgReader(Container[PackURI]):
|
||||
"""Base class for physical package reader objects."""
|
||||
|
||||
def __contains__(self, item: object) -> bool:
|
||||
"""Must be implemented by each subclass."""
|
||||
raise NotImplementedError( # pragma: no cover
|
||||
"`%s` must implement `.__contains__()`" % type(self).__name__
|
||||
)
|
||||
|
||||
def __getitem__(self, pack_uri: PackURI) -> bytes:
|
||||
"""Blob for part corresponding to `pack_uri`."""
|
||||
raise NotImplementedError( # pragma: no cover
|
||||
f"`{type(self).__name__}` must implement `.__contains__()`"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader:
|
||||
"""Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`."""
|
||||
# --- for pkg_file other than str, assume it's a stream and pass it to Zip
|
||||
# --- reader to sort out
|
||||
if not isinstance(pkg_file, str):
|
||||
return _ZipPkgReader(pkg_file)
|
||||
|
||||
# --- otherwise we treat `pkg_file` as a path ---
|
||||
if os.path.isdir(pkg_file):
|
||||
return _DirPkgReader(pkg_file)
|
||||
|
||||
if zipfile.is_zipfile(pkg_file):
|
||||
return _ZipPkgReader(pkg_file)
|
||||
|
||||
raise PackageNotFoundError("Package not found at '%s'" % pkg_file)
|
||||
|
||||
|
||||
class _DirPkgReader(_PhysPkgReader):
|
||||
"""Implements |PhysPkgReader| interface for OPC package extracted into directory.
|
||||
|
||||
`path` is the path to a directory containing an expanded package.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str):
|
||||
self._path = os.path.abspath(path)
|
||||
|
||||
def __contains__(self, pack_uri: object) -> bool:
|
||||
"""Return True when part identified by `pack_uri` is present in zip archive."""
|
||||
if not isinstance(pack_uri, PackURI):
|
||||
return False
|
||||
return os.path.exists(posixpath.join(self._path, pack_uri.membername))
|
||||
|
||||
def __getitem__(self, pack_uri: PackURI) -> bytes:
|
||||
"""Return bytes of file corresponding to `pack_uri` in package directory."""
|
||||
path = os.path.join(self._path, pack_uri.membername)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
except IOError:
|
||||
raise KeyError("no member '%s' in package" % pack_uri)
|
||||
|
||||
|
||||
class _ZipPkgReader(_PhysPkgReader):
|
||||
"""Implements |PhysPkgReader| interface for a zip-file OPC package."""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes]):
|
||||
self._pkg_file = pkg_file
|
||||
|
||||
def __contains__(self, pack_uri: object) -> bool:
|
||||
"""Return True when part identified by `pack_uri` is present in zip archive."""
|
||||
return pack_uri in self._blobs
|
||||
|
||||
def __getitem__(self, pack_uri: PackURI) -> bytes:
|
||||
"""Return bytes for part corresponding to `pack_uri`.
|
||||
|
||||
Raises |KeyError| if no matching member is present in zip archive.
|
||||
"""
|
||||
if pack_uri not in self._blobs:
|
||||
raise KeyError("no member '%s' in package" % pack_uri)
|
||||
return self._blobs[pack_uri]
|
||||
|
||||
@lazyproperty
|
||||
def _blobs(self) -> dict[PackURI, bytes]:
|
||||
"""dict mapping partname to package part binaries."""
|
||||
with zipfile.ZipFile(self._pkg_file, "r") as z:
|
||||
return {PackURI("/%s" % name): z.read(name) for name in z.namelist()}
|
||||
|
||||
|
||||
class _PhysPkgWriter:
|
||||
"""Base class for physical package writer objects."""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter:
|
||||
"""Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`.
|
||||
|
||||
Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented
|
||||
or even a `_StreamPkgWriter`.
|
||||
"""
|
||||
return _ZipPkgWriter(pkg_file)
|
||||
|
||||
def write(self, pack_uri: PackURI, blob: bytes) -> None:
|
||||
"""Write `blob` to package with membername corresponding to `pack_uri`."""
|
||||
raise NotImplementedError( # pragma: no cover
|
||||
f"`{type(self).__name__}` must implement `.write()`"
|
||||
)
|
||||
|
||||
|
||||
class _ZipPkgWriter(_PhysPkgWriter):
|
||||
"""Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package."""
|
||||
|
||||
def __init__(self, pkg_file: str | IO[bytes]):
|
||||
self._pkg_file = pkg_file
|
||||
|
||||
def __enter__(self) -> _ZipPkgWriter:
|
||||
"""Enable use as a context-manager. Opening zip for writing happens here."""
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc: list[Any]) -> None:
|
||||
"""Close the zip archive on exit from context.
|
||||
|
||||
Closing flushes any pending physical writes and releasing any resources it's using.
|
||||
"""
|
||||
self._zipf.close()
|
||||
|
||||
def write(self, pack_uri: PackURI, blob: bytes) -> None:
|
||||
"""Write `blob` to zip package with membername corresponding to `pack_uri`."""
|
||||
self._zipf.writestr(pack_uri.membername, blob)
|
||||
|
||||
@lazyproperty
|
||||
def _zipf(self) -> zipfile.ZipFile:
|
||||
"""`ZipFile` instance open for writing."""
|
||||
return zipfile.ZipFile(
|
||||
self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False
|
||||
)
|
||||
|
||||
|
||||
class _ContentTypesItem:
|
||||
"""Composes content-types "part" ([Content_Types].xml) for a collection of parts."""
|
||||
|
||||
def __init__(self, parts: Sequence[Part]):
|
||||
self._parts = parts
|
||||
|
||||
@classmethod
|
||||
def xml_for(cls, parts: Sequence[Part]) -> CT_Types:
|
||||
"""Return content-types XML mapping each part in `parts` to a content-type.
|
||||
|
||||
The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package.
|
||||
"""
|
||||
return cls(parts)._xml
|
||||
|
||||
@lazyproperty
|
||||
def _xml(self) -> CT_Types:
|
||||
"""lxml.etree._Element containing the content-types item.
|
||||
|
||||
This XML object is suitable for serialization to the `[Content_Types].xml` item for an OPC
|
||||
package. Although the sequence of elements is not strictly significant, as an aid to
|
||||
testing and readability Default elements are sorted by extension and Override elements are
|
||||
sorted by partname.
|
||||
"""
|
||||
defaults, overrides = self._defaults_and_overrides
|
||||
_types_elm = CT_Types.new()
|
||||
|
||||
for ext, content_type in sorted(defaults.items()):
|
||||
_types_elm.add_default(ext, content_type)
|
||||
for partname, content_type in sorted(overrides.items()):
|
||||
_types_elm.add_override(partname, content_type)
|
||||
|
||||
return _types_elm
|
||||
|
||||
@lazyproperty
|
||||
def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]:
|
||||
"""pair of dict (defaults, overrides) accounting for all parts.
|
||||
|
||||
`defaults` is {ext: content_type} and overrides is {partname: content_type}.
|
||||
"""
|
||||
defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML)
|
||||
overrides: dict[PackURI, str] = {}
|
||||
|
||||
for part in self._parts:
|
||||
partname, content_type = part.partname, part.content_type
|
||||
ext = partname.ext
|
||||
if (ext.lower(), content_type) in default_content_types:
|
||||
defaults[ext] = content_type
|
||||
else:
|
||||
overrides[partname] = content_type
|
||||
|
||||
return defaults, overrides
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Objects shared by modules in the pptx.opc sub-package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class CaseInsensitiveDict(dict):
|
||||
"""Mapping type like dict except it matches key without respect to case.
|
||||
|
||||
For example, D['A'] == D['a']. Note this is not general-purpose, just complete
|
||||
enough to satisfy opc package needs. It assumes str keys for example.
|
||||
"""
|
||||
|
||||
def __contains__(self, key):
|
||||
return super(CaseInsensitiveDict, self).__contains__(key.lower())
|
||||
|
||||
def __getitem__(self, key):
|
||||
return super(CaseInsensitiveDict, self).__getitem__(key.lower())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500."""
|
||||
|
||||
from pptx.opc.constants import CONTENT_TYPE as CT
|
||||
|
||||
default_content_types = (
|
||||
("bin", CT.PML_PRINTER_SETTINGS),
|
||||
("bin", CT.SML_PRINTER_SETTINGS),
|
||||
("bin", CT.WML_PRINTER_SETTINGS),
|
||||
("bmp", CT.BMP),
|
||||
("emf", CT.X_EMF),
|
||||
("fntdata", CT.X_FONTDATA),
|
||||
("gif", CT.GIF),
|
||||
("jpe", CT.JPEG),
|
||||
("jpeg", CT.JPEG),
|
||||
("jpg", CT.JPEG),
|
||||
("mov", CT.MOV),
|
||||
("mp4", CT.MP4),
|
||||
("mpg", CT.MPG),
|
||||
("png", CT.PNG),
|
||||
("rels", CT.OPC_RELATIONSHIPS),
|
||||
("tif", CT.TIFF),
|
||||
("tiff", CT.TIFF),
|
||||
("vid", CT.VIDEO),
|
||||
("wdp", CT.MS_PHOTO),
|
||||
("wmf", CT.X_WMF),
|
||||
("wmv", CT.WMV),
|
||||
("xlsx", CT.SML_SHEET),
|
||||
("xml", CT.XML),
|
||||
)
|
||||
|
||||
|
||||
image_content_types = {
|
||||
"bmp": CT.BMP,
|
||||
"emf": CT.X_EMF,
|
||||
"gif": CT.GIF,
|
||||
"jpe": CT.JPEG,
|
||||
"jpeg": CT.JPEG,
|
||||
"jpg": CT.JPEG,
|
||||
"png": CT.PNG,
|
||||
"tif": CT.TIFF,
|
||||
"tiff": CT.TIFF,
|
||||
"wdp": CT.MS_PHOTO,
|
||||
"wmf": CT.X_WMF,
|
||||
}
|
||||
@@ -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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user