Skip to content

Commit 432dd15

Browse files
committed
drawing: add image extraction from Drawing
1 parent 19175ad commit 432dd15

File tree

4 files changed

+115
-3
lines changed

4 files changed

+115
-3
lines changed

features/cmt-props.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ Feature: Get comment properties
3030
Then para_text is the text of the first paragraph in the comment
3131

3232

33-
@wip
3433
Scenario: Retrieve embedded image from a comment
3534
Given a Comment object containing an embedded image
3635
Then I can extract the image from the comment

src/docx/drawing/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
if TYPE_CHECKING:
1111
import docx.types as t
12+
from docx.image.image import Image
1213

1314

1415
class Drawing(Parented):
@@ -18,3 +19,41 @@ def __init__(self, drawing: CT_Drawing, parent: t.ProvidesStoryPart):
1819
super().__init__(parent)
1920
self._parent = parent
2021
self._drawing = self._element = drawing
22+
23+
@property
24+
def has_picture(self) -> bool:
25+
"""True when `drawing` contains an embedded picture.
26+
27+
A drawing can contain a picture, but it can also contain a chart, SmartArt, or a
28+
drawing canvas. Methods related to a picture, like `.image`, will raise when the drawing
29+
does not contain a picture. Use this value to determine whether image methods will succeed.
30+
31+
This value is `False` when a linked picture is present. This should be relatively rare and
32+
the image would only be retrievable from the filesystem.
33+
34+
Note this does not distinguish between inline and floating images. The presence of either
35+
one will cause this value to be `True`.
36+
"""
37+
xpath_expr = (
38+
# -- an inline picture --
39+
"./wp:inline/a:graphic/a:graphicData/pic:pic"
40+
# -- a floating picture --
41+
" | ./wp:anchor/a:graphic/a:graphicData/pic:pic"
42+
)
43+
# -- xpath() will return a list, empty if there are no matches --
44+
return bool(self._drawing.xpath(xpath_expr))
45+
46+
@property
47+
def image(self) -> Image:
48+
"""An `Image` proxy object for the image in this (picture) drawing.
49+
50+
Raises `ValueError` when this drawing does contains something other than a picture. Use
51+
`.has_picture` to qualify drawing objects before using this property.
52+
"""
53+
picture_rIds = self._drawing.xpath(".//pic:blipFill/a:blip/@r:embed")
54+
if not picture_rIds:
55+
raise ValueError("drawing does not contain a picture")
56+
rId = picture_rIds[0]
57+
doc_part = self.part
58+
image_part = doc_part.related_parts[rId]
59+
return image_part.image

tests/test_comments.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# pyright: reportPrivateUsage=false
22

3-
"""Unit test suite for the docx.comments module."""
3+
"""Unit test suite for the `docx.comments` module."""
44

55
from __future__ import annotations
66

@@ -21,7 +21,7 @@
2121

2222

2323
class DescribeComments:
24-
"""Unit-test suite for `docx.comments.Comments`."""
24+
"""Unit-test suite for `docx.comments.Comments` objects."""
2525

2626
@pytest.mark.parametrize(
2727
("cxml", "count"),

tests/test_drawing.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# pyright: reportPrivateUsage=false
2+
3+
"""Unit test suite for the `docx.drawing` module."""
4+
5+
from __future__ import annotations
6+
7+
from typing import cast
8+
9+
import pytest
10+
11+
from docx.drawing import Drawing
12+
from docx.image.image import Image
13+
from docx.oxml.drawing import CT_Drawing
14+
from docx.parts.document import DocumentPart
15+
from docx.parts.image import ImagePart
16+
17+
from .unitutil.cxml import element
18+
from .unitutil.mock import FixtureRequest, Mock, instance_mock
19+
20+
21+
class DescribeDrawing:
22+
"""Unit-test suite for `docx.drawing.Drawing` objects."""
23+
24+
@pytest.mark.parametrize(
25+
("cxml", "expected_value"),
26+
[
27+
("w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic", True),
28+
("w:drawing/wp:anchor/a:graphic/a:graphicData/pic:pic", True),
29+
("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp", False),
30+
("w:drawing/wp:anchor/a:graphic/a:graphicData/a:chart", False),
31+
],
32+
)
33+
def it_knows_when_it_contains_a_Picture(
34+
self, cxml: str, expected_value: bool, document_part_: Mock
35+
):
36+
drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_)
37+
assert drawing.has_picture == expected_value
38+
39+
def it_provides_access_to_the_image_in_a_Picture_drawing(
40+
self, document_part_: Mock, image_part_: Mock, image_: Mock
41+
):
42+
image_part_.image = image_
43+
document_part_.part.related_parts = {"rId1": image_part_}
44+
cxml = (
45+
"w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip{r:embed=rId1}"
46+
)
47+
drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_)
48+
49+
image = drawing.image
50+
51+
assert image is image_
52+
53+
def but_it_raises_when_the_drawing_does_not_contain_a_Picture(self, document_part_: Mock):
54+
drawing = Drawing(
55+
cast(CT_Drawing, element("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp")),
56+
document_part_,
57+
)
58+
59+
with pytest.raises(ValueError, match="drawing does not contain a picture"):
60+
drawing.image
61+
62+
# -- fixtures --------------------------------------------------------------------------------
63+
64+
@pytest.fixture
65+
def document_part_(self, request: FixtureRequest):
66+
return instance_mock(request, DocumentPart)
67+
68+
@pytest.fixture
69+
def image_(self, request: FixtureRequest):
70+
return instance_mock(request, Image)
71+
72+
@pytest.fixture
73+
def image_part_(self, request: FixtureRequest):
74+
return instance_mock(request, ImagePart)

0 commit comments

Comments
 (0)