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
15 changes: 15 additions & 0 deletions docs/dev/analysis/shp-shapes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ Proposed protocol::
>>> shape.name
u'T501 - Foo; B. Baz; 2014'

``Shape.alternative_text``
-------------------------------------

``Shape.alternative_text`` is a read-write property. It's the alternative
text for the shape and is readable by screen readers.

Proposed protocol::

>>> shape.alt_text
'Company Logo'
>>> shape.alt_text = 'Trademark'
>>> shape.alt_text
'Trademark'



:attr:`Shape.rotation`
----------------------
Expand Down
32 changes: 32 additions & 0 deletions features/shp-alt-text.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Feature: Shape alternative text
In order to provide accessibility features
As a developer using python-pptx
I need to get and set alternative text for shapes

Scenario: Get alt_text from shape with no alternative text
Given a shape with no alternative text
Then the alt_text property should be None

Scenario: Get alt_text from shape with existing alternative text
Given a shape with alternative text "A red rectangle"
Then the alt_text property should be "A red rectangle"

Scenario: Set alt_text on a shape
Given a shape with no alternative text
When I set the alt_text property to "Accessibility description"
Then the alt_text property should be "Accessibility description"

Scenario: Update alt_text on a shape
Given a shape with alternative text "Old description"
When I set the alt_text property to "New description"
Then the alt_text property should be "New description"

Scenario: Clear alt_text from a shape
Given a shape with alternative text "Some description"
When I set the alt_text property to an empty string
Then the alt_text property should be an empty string

Scenario: Delete alt_text from a shape
Given a shape with alternative text "Some description"
When I delete the alt_text property
Then the alt_text property should be None
45 changes: 45 additions & 0 deletions features/steps/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,48 @@ def then_the_table_appears_in_the_slide(context):
prs = Presentation(saved_pptx_path)
expected_table_graphic_frame = prs.slides[0].shapes[0]
assert expected_table_graphic_frame.has_table


# alt_text step definitions ===============================================

@given("a shape with no alternative text")
def given_a_shape_with_no_alternative_text(context):
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
context.shape = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, Inches(1), Inches(1), Inches(2), Inches(1)
)

@given('a shape with alternative text "{text}"')
def given_a_shape_with_alternative_text(context, text):
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
shape = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, Inches(1), Inches(1), Inches(2), Inches(1)
)
shape.alt_text = text
context.shape = shape

@when('I set the alt_text property to "{text}"')
def when_I_set_the_alt_text_property_to(context, text):
context.shape.alt_text = text

@then('the alt_text property should be "{expected_text}"')
def then_the_alt_text_property_should_be(context, expected_text):
assert context.shape.alt_text == expected_text

@when('I set the alt_text property to an empty string')
def when_I_set_the_alt_text_property_to_empty_string(context):
context.shape.alt_text = ""

@then("the alt_text property should be an empty string")
def then_the_alt_text_property_should_be_an_empty_string(context):
assert context.shape.alt_text == ""

@when('I delete the alt_text property')
def when_I_delete_the_alt_text_property(context):
del context.shape.alt_text

@then("the alt_text property should be None")
def then_the_alt_text_property_should_be_None(context):
assert context.shape.alt_text is None
15 changes: 15 additions & 0 deletions src/pptx/shapes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ def width(self) -> Length:
def width(self, value: Length):
self._element.cx = value

@property
def alt_text(self):
"""
Read/write. Alternate text string for this shape.
"""
return self._element._nvXxPr.cNvPr.attrib.get("descr")

@alt_text.setter
def alt_text(self, value):
self._element._nvXxPr.cNvPr.attrib["descr"] = value

@alt_text.deleter
def alt_text(self):
del self._element._nvXxPr.cNvPr.attrib["descr"]


class _PlaceholderFormat(ElementProxy):
"""Provides properties specific to placeholders, such as the placeholder type.
Expand Down
104 changes: 104 additions & 0 deletions tests/shapes/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def it_can_change_its_name(self, name_set_fixture):
shape.name = new_value
assert shape._element.xml == expected_xml


@pytest.mark.parametrize(
("shape_cxml", "expected_x", "expected_y"),
[
Expand Down Expand Up @@ -108,6 +109,20 @@ def part(self) -> XmlPart:

return FakeProvidesPart()

def it_knows_its_alt_text(self, alt_text_get_fixture):
shape, alt_text = alt_text_get_fixture
assert shape.alt_text == alt_text

def it_can_change_its_alt_text(self, alt_text_set_fixture):
shape, new_value, expected_xml = alt_text_set_fixture
shape.alt_text = new_value
assert shape._element.xml == expected_xml

def it_can_delete_its_alt_text(self, alt_text_del_fixture):
shape, expected_xml = alt_text_del_fixture
del shape.alt_text
assert shape._element.xml == expected_xml

def it_can_change_its_position(self, position_set_fixture):
shape, left, top, expected_xml = position_set_fixture
shape.left = left
Expand Down Expand Up @@ -301,6 +316,91 @@ def name_set_fixture(self, request):
expected_xml = xml(expected_xSp_cxml)
return shape, new_value, expected_xml

@pytest.fixture
def alt_text_get_fixture(self, shape_alt_text):
shape_elm = (
an_sp()
.with_nsdecls()
.with_child(an_nvSpPr().with_child(a_cNvPr().with_descr(shape_alt_text)))
).element
shape = BaseShape(shape_elm, None)
return shape, shape_alt_text

@pytest.fixture(
params=[
(
"p:sp/p:nvSpPr/p:cNvPr{id=1,descr=foo}",
Shape,
"AltText1",
"p:sp/p:nvSpPr/p:cNvPr{id=1,descr=AltText1}",
),
(
"p:grpSp/p:nvGrpSpPr/p:cNvPr{id=2,descr=bar}",
BaseShape,
"AltText2",
"p:grpSp/p:nvGrpSpPr/p:cNvPr{id=2,descr=AltText2}",
),
(
"p:graphicFrame/p:nvGraphicFramePr/p:cNvPr{id=3,descr=baz}",
GraphicFrame,
"AltText3",
"p:graphicFrame/p:nvGraphicFramePr/p:cNvPr{id=3,descr=AltText3}",
),
(
"p:cxnSp/p:nvCxnSpPr/p:cNvPr{id=4,descr=boo}",
BaseShape,
"AltText4",
"p:cxnSp/p:nvCxnSpPr/p:cNvPr{id=4,descr=AltText4}",
),
(
"p:pic/p:nvPicPr/p:cNvPr{id=5,descr=far}",
Picture,
"AltText5",
"p:pic/p:nvPicPr/p:cNvPr{id=5,descr=AltText5}",
),
]
)
def alt_text_set_fixture(self, request):
xSp_cxml, ShapeCls, new_value, expected_xSp_cxml = request.param
shape = ShapeCls(element(xSp_cxml), None)
expected_xml = xml(expected_xSp_cxml)
return shape, new_value, expected_xml

@pytest.fixture(
params=[
(
"p:sp/p:nvSpPr/p:cNvPr{id=1,descr=foo}",
Shape,
"p:sp/p:nvSpPr/p:cNvPr{id=1}",
),
(
"p:grpSp/p:nvGrpSpPr/p:cNvPr{id=2,descr=bar}",
BaseShape,
"p:grpSp/p:nvGrpSpPr/p:cNvPr{id=2}",
),
(
"p:graphicFrame/p:nvGraphicFramePr/p:cNvPr{id=3,descr=baz}",
GraphicFrame,
"p:graphicFrame/p:nvGraphicFramePr/p:cNvPr{id=3}",
),
(
"p:cxnSp/p:nvCxnSpPr/p:cNvPr{id=4,descr=boo}",
BaseShape,
"p:cxnSp/p:nvCxnSpPr/p:cNvPr{id=4}",
),
(
"p:pic/p:nvPicPr/p:cNvPr{id=5,descr=far}",
Picture,
"p:pic/p:nvPicPr/p:cNvPr{id=5}",
),
]
)
def alt_text_del_fixture(self, request):
xSp_cxml, ShapeCls, expected_xSp_cxml = request.param
shape = ShapeCls(element(xSp_cxml), None)
expected_xml = xml(expected_xSp_cxml)
return shape, expected_xml

@pytest.fixture
def part_fixture(self, shapes_):
parent_ = shapes_
Expand Down Expand Up @@ -534,6 +634,10 @@ def shape_id(self):
def shape_name(self):
return "Foobar 41"

@pytest.fixture
def shape_alt_text(self):
return "Alternative text description"

@pytest.fixture
def shapes_(self, request):
return instance_mock(request, SlideShapes)
Expand Down