Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/user/text.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,33 @@ second and third indented (like sub-bullets) under the first::
p.level = 1


Applying paragraph bullet formatting
-----------------------------

The following continues from the previous example, changes the first bullet
to an "x", and changes the second and third sub-bullets to be numbered::

from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE
from pptx.util import BulletStyle

p = text_frame.paragraphs[0]
p.bullet = BulletStyle.custom("x")

for p in text_frame.paragraphs[1:]:
p.bullet = BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ARABIC_PERIOD)

The ``.bullet`` attribute can also be used to remove bullet formatting::

p = text_frame.add_paragraph()
p.text = "This is not a bullet!"
p.bullet = BulletStyle.NO_BULLET

Finally, the attribute can also be used to revert a paragraph back to the
slide's default bullet configuration::

p.text = "Now it's a bullet again."
p.bullet = BulletStyle.DEFAULT

Applying character formatting
-----------------------------

Expand Down
28 changes: 26 additions & 2 deletions features/steps/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from helpers import test_pptx

from pptx import Presentation
from pptx.enum.text import PP_ALIGN
from pptx.util import Emu
from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE, PP_ALIGN
from pptx.util import BulletStyle, Emu

# given ===================================================

Expand Down Expand Up @@ -138,6 +138,18 @@ def when_set_hyperlink_address(context):
hlink.address = context.address


@when("I assign paragraph.bullet = {value_str}")
def when_I_assign_value_to_paragraph_bullet(context, value_str):
value = {
"x": BulletStyle.custom("x"),
"Default": BulletStyle.DEFAULT,
"No bullet": BulletStyle.NO_BULLET,
"hindiAlphaPeriod": BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD),
}[value_str]
paragraph = context.paragraph
paragraph.bullet = value


# then ====================================================


Expand Down Expand Up @@ -246,3 +258,15 @@ def then_run_text_is_not_a_hyperlink(context):
@then("the font name matches the typeface I set")
def then_font_name_matches_typeface_I_set(context):
assert context.font.name == "Verdana"

@then("paragraph.bullet == {value}")
def then_paragraph_bullet_is_value(context, value):
expected = {
"x": BulletStyle.custom("x"),
"Default": BulletStyle.DEFAULT,
"No bullet": BulletStyle.NO_BULLET,
"hindiAlphaPeriod": BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD),
}[value]

actual = context.paragraph.bullet
assert actual == expected, 'paragraph.bullet == "%s"' % actual
13 changes: 13 additions & 0 deletions features/txt-paragraph.feature
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,16 @@ Feature: Change paragraph properties
| "a\vb\vc" | "a\vb\vc" |
| "a\nb\vc" | "a\vb\vc" |
| "a\x1Bc" | "a_x001B_c" |


Scenario Outline: _Paragraph.bullet setter
Given a _Paragraph object as paragraph
When I assign paragraph.bullet = <value>
Then paragraph.bullet == <value>

Examples: _Paragraph assigned bullet replacement cases
| value | expected-value |
| x | x |
| No bullet | No bullet |
| Default | Default |
| hindiAlphaPeriod | hindiAlphaPeriod |
67 changes: 67 additions & 0 deletions src/pptx/enum/text.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Enumerations used by text and related objects."""

from __future__ import annotations
from enum import Enum, auto

from pptx.enum.base import BaseEnum, BaseXmlEnum

Expand Down Expand Up @@ -228,3 +229,69 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):


PP_ALIGN = PP_PARAGRAPH_ALIGNMENT


class BulletStyleType(Enum):
"""Types of bullet styles."""
NO_BULLET = auto()
CUSTOM = auto()
NUMBERED = auto()
DEFAULT = auto()


class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum):
"""Specifies the style of numbered bullets.

Example::

from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE

shape.paragraphs[0].bullet = MSO_NUMBERED_BULLET_STYLE.BULLET_ALPHA_UC_PERIOD

MS API Name: `MsoNumberedBulletStyle`

https://learn.microsoft.com/sv-se/office/vba/api/office.msonumberedbulletstyle
"""

ALPHA_LC_PAREN_BOTH = (8, "alphaLCParenBoth", "Lowercase alphabetical bullet with opening and closing parentheses.")
ALPHA_LC_PAREN_RIGHT = (9, "alphaLCParenRight", "Lowercase alphabetical bullet with closing parenthesis.")
ALPHA_LC_PERIOD = (0, "alphaLCPeriod", "Lowercase alphabetical bullet with period.|")
ALPHA_UC_PAREN_BOTH = (10, "alphaUCParenBoth", "Uppercase alphabetical bullet with opening and closing parentheses.")
ALPHA_UC_PAREN_RIGHT = (11, "alphaUCParenRight", "Uppercase alphabetical bullet with closing parenthesis.")
ALPHA_UC_PERIOD = (1, "alphaUCPeriod", "Uppercase alphabetical bullet with period.")
ARABIC_ABJAD_DASH = (24, "arabicAbjadDash", "Arabic Abjad bullet with a dash.")
ARABIC_ALPHA_DASH = (23, "arabicAlphaDash", "Arabic alphabetical bullet with a dash.")
ARABIC_DB_PERIOD = (29, "arabicDBPeriod", "Arabic DB bullet with period.")
ARABIC_DB_PLAIN = (28, "arabicDBPlain", "Plain Arabic DB bullet.")
ARABIC_PAREN_BOTH = (12, "arabicParenBoth", "Arabic bullet with opening and closing parentheses.")
ARABIC_PAREN_RIGHT = (2, "arabicParenRight", "Arabic bullet with closing parenthesis.")
ARABIC_PERIOD = (3, "arabicPeriod", "Arabic bullet with period.")
ARABIC_PLAIN = (13, "arabicPlain", "Plain Arabic bullet.")
CIRCLE_NUM_DB_PLAIN = (18, "circleNumDBPlain", "Circled number bullet.")
CIRCLE_NUM_WD_BLACK_PLAIN = (20, "circleNumWDBlackPlain", "Circled number WD black bullet.")
CIRCLE_NUM_WD_WHITE_PLAIN = (19, "circleNumWDWhitePlain", "Circled number WD white bullet.")
HEBREW_ALPHA_DASH = (25, "hebrewAlphaDash", "Hebrew alphabetical bullet with dash.")
HINDI_ALPHA1_PERIOD = (40, "hindiAlpha1Period", "Hindi alphabetical bullet 1 with period.")
HINDI_ALPHA_PERIOD = (36, "hindiAlphaPeriod", "Hindi alphabetical bullet with period.")
HINDI_NUM_PAREN_RIGHT = (39, "hindiNumParenRight", "Hindi numbered bullet with closing parenthesis.")
HINDI_NUM_PERIOD = (37, "hindiNumPeriod", "Hindi numbered bullet with period.")
KANJI_KOREAN_PERIOD = (27, "kanjiKoreanPeriod", "Korean Kanji bullet with period.")
KANJI_KOREAN_PLAIN = (26, "kanjiKoreanPlain", "Korean Kanji bullet.")
KANJI_SIMP_CHIN_DB_PERIOD = (38, "kanjiSimpChinDBPeriod", "Simplified Chinese Kanji bulllet with period.")
ROMAN_LC_PAREN_BOTH = (4, "romanLCParenBoth", "Lowercase roman bullet with opening and closing parentheses.")
ROMAN_LC_PAREN_RIGHT = (5, "romanLCParenRight", "Lowercase roman bullet with closing parenthesis.")
ROMAN_LC_PERIOD = (6, "romanLCPeriod", "Lowercase roman bullet with period.")
ROMAN_UC_PAREN_BOTH = (14, "romanUCParenBoth", "Uppercase roman bullet with opening and closing parentheses.")
ROMAN_UC_PAREN_RIGHT = (15, "romanUCParenRight", "Uppercase roman bullet with closing parenthesis.")
ROMAN_UC_PERIOD = (7, "romanUCPeriod", "Uppercase roman bullet with period.")
SIMP_CHIN_PERIOD = (17, "simpChinPeriod", "Simplified Chinese bulllet with period.")
SIMP_CHIN_PLAIN = (16, "simpChinPlain", "Simplified Chinese bullet.")
STYLE_MIXED = (-2, "styleMixed", "Return value only; indicates a combination of the other states. ")
THAI_ALPHA_PAREN_BOTH = (32, "thaiAlphaParenBoth", "Thai alphabetical bullet with opening and closing parentheses.")
THAI_ALPHA_PAREN_RIGHT = (31, "thaiAlphaParenRight", "Thai alphabetical bullet with closing parenthesis.")
THAI_ALPHA_PERIOD = (30, "thaiAlphaPeriod", "Thai alphabetical bullet with period.")
THAI_NUM_PAREN_BOTH = (35, "thaiNumParenBoth", "Thai numerical bullet with opening and closing parentheses.")
THAI_NUM_PAREN_RIGHT = (34, "thaiNumParenRight", "Thai numerical bullet with closing parenthesis.")
THAI_NUM_PERIOD = (33, "thaiNumPeriod", "Thai numerical bullet with period.")
TRAD_CHIN_PERIOD = (22, "tradChinPeriod", "Traditional Chinese bulllet with period.")
TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.")
6 changes: 6 additions & 0 deletions src/pptx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):

from pptx.oxml.text import ( # noqa: E402
CT_RegularTextRun,
CT_TextAutoNumberBullet,
CT_TextBody,
CT_TextBodyProperties,
CT_TextCharacterProperties,
Expand All @@ -457,6 +458,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
CT_TextSpacing,
CT_TextSpacingPercent,
CT_TextSpacingPoint,
CT_TextNoBullet,
CT_TextCharBullet,
)

register_element_cls("a:bodyPr", CT_TextBodyProperties)
Expand All @@ -476,6 +479,9 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
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:buNone', CT_TextNoBullet)
register_element_cls('a:buAutoNum', CT_TextAutoNumberBullet)
register_element_cls('a:buChar', CT_TextCharBullet)
register_element_cls("a:txBody", CT_TextBody)
register_element_cls("c:txPr", CT_TextBody)
register_element_cls("p:txBody", CT_TextBody)
Expand Down
82 changes: 81 additions & 1 deletion src/pptx/oxml/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from pptx.enum.lang import MSO_LANGUAGE_ID
from pptx.enum.text import (
MSO_AUTO_SIZE,
MSO_NUMBERED_BULLET_STYLE,
MSO_TEXT_UNDERLINE_TYPE,
MSO_VERTICAL_ANCHOR,
PP_PARAGRAPH_ALIGNMENT,
BulletStyleType,
)
from pptx.exc import InvalidXmlError
from pptx.oxml import parse_xml
Expand All @@ -26,6 +28,7 @@
ST_TextTypeface,
ST_TextWrappingType,
XsdBoolean,
XsdString,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Expand All @@ -38,7 +41,7 @@
ZeroOrOne,
ZeroOrOneChoice,
)
from pptx.util import Emu, Length
from pptx.util import BulletStyle, Emu, Length

if TYPE_CHECKING:
from pptx.oxml.action import CT_Hyperlink
Expand Down Expand Up @@ -466,9 +469,15 @@ class CT_TextParagraphProperties(BaseOxmlElement):
_add_lnSpc: Callable[[], CT_TextSpacing]
_add_spcAft: Callable[[], CT_TextSpacing]
_add_spcBef: Callable[[], CT_TextSpacing]
_add_buNone: Callable[[], CT_TextNoBullet]
_add_buAutoNum: Callable[[], CT_TextAutoNumberBullet]
_add_buChar: Callable[[], CT_TextCharBullet]
_remove_lnSpc: Callable[[], None]
_remove_spcAft: Callable[[], None]
_remove_spcBef: Callable[[], None]
_remove_buNone: Callable[[], None]
_remove_buAutoNum: Callable[[], CT_TextAutoNumberBullet]
_remove_buChar: Callable[[], None]

_tag_seq = (
"a:lnSpc",
Expand Down Expand Up @@ -501,6 +510,15 @@ class CT_TextParagraphProperties(BaseOxmlElement):
defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:defRPr", successors=_tag_seq[16:]
)
buNone: CT_TextNoBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:buNone", successors=_tag_seq[11:]
)
buAutoNum: CT_TextAutoNumberBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:buAutoNum", successors=_tag_seq[12:]
)
buChar: CT_TextCharBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:buChar", successors=_tag_seq[13:]
)
lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"lvl", ST_TextIndentLevelType, default=0
)
Expand Down Expand Up @@ -534,6 +552,40 @@ def line_spacing(self, value: float | Length | None):
else:
self._add_lnSpc().set_spcPct(value)

@property
def bullet(self) -> BulletStyle:
"""The style of bullet used for this paragraph."""
buNone = self.buNone
buChar = self.buChar
buAutoNum = self.buAutoNum

if buChar is not None:
return BulletStyle.custom(buChar.char)
elif buAutoNum is not None:
return BulletStyle.numbered(buAutoNum.val)
elif buNone is not None:
return BulletStyle.NO_BULLET
else:
return BulletStyle.DEFAULT

@bullet.setter
def bullet(self, value: BulletStyle):
self._remove_buNone()
self._remove_buChar()
self._remove_buAutoNum()

if value == BulletStyle.DEFAULT:
return
elif value == BulletStyle.NO_BULLET:
self._add_buNone()
elif value.style == BulletStyleType.CUSTOM:
buChar = self._add_buChar()
buChar.char = cast(str, value.value)
elif value.style == BulletStyleType.NUMBERED:
buAutoNum = self._add_buAutoNum()
buAutoNum.val = cast(MSO_NUMBERED_BULLET_STYLE, value.value)


@property
def space_after(self) -> Length | None:
"""The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`."""
Expand Down Expand Up @@ -616,3 +668,31 @@ class CT_TextSpacingPoint(BaseOxmlElement):
val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"val", ST_TextSpacingPoint
)


class CT_TextNoBullet(BaseOxmlElement):
"""
<a:buNone> element, specifying that a paragraph should not be bulleted.
"""
pass


class CT_TextCharBullet(BaseOxmlElement):
"""
<a:buChar> element, specifying that a paragraph should have a character bullet.
"""

char: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"char", XsdString
)


class CT_TextAutoNumberBullet(BaseOxmlElement):
"""
<a:buAutoNum> element, specifying that a paragraph should have an automatically
incremented bullet of the specified `type`.
"""

val: MSO_NUMBERED_BULLET_STYLE = RequiredAttribute( # pyright: ignore[reportAssignmentType]
"type", MSO_NUMBERED_BULLET_STYLE
)
26 changes: 24 additions & 2 deletions src/pptx/text/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from pptx.dml.fill import FillFormat
from pptx.enum.dml import MSO_FILL
from pptx.enum.lang import MSO_LANGUAGE_ID
from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR
from pptx.enum.text import MSO_AUTO_SIZE, MSO_NUMBERED_BULLET_STYLE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
from pptx.oxml.simpletypes import ST_TextWrappingType
from pptx.shapes import Subshape
from pptx.text.fonts import FontFiles
from pptx.text.layout import TextFitter
from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty
from pptx.util import BulletStyle, Centipoints, Emu, Length, Pt, lazyproperty

if TYPE_CHECKING:
from pptx.dml.color import ColorFormat
Expand Down Expand Up @@ -547,6 +547,28 @@ def line_spacing(self, value: int | float | Length | None):
pPr = self._p.get_or_add_pPr()
pPr.line_spacing = value

@property
def bullet(self) -> BulletStyle:
"""The type of bullet, if any, defined for this paragraph.

``BulletStyle.NO_BULLET`` indicates that bullets are explicitly disabled
a paragraph. ``BulletStyle.DEFAULT`` indicates that whether the paragraph
is rendered as a bullet is defined in the slide master or layout.

The methods ``BulletStyle.custom`` and ``BulletStyle.numbered`` can be
used to create ``BulletStyle``s that control what kind of bullet is used
for the paragraph.
"""
pPr = self._p.pPr
if pPr is None:
return BulletStyle.DEFAULT
return pPr.bullet

@bullet.setter
def bullet(self, value: BulletStyle):
pPr = self._p.get_or_add_pPr()
pPr.bullet = value

@property
def runs(self) -> tuple[_Run, ...]:
"""Sequence of runs in this paragraph."""
Expand Down
Loading