Skip to content

Commit d7001ed

Browse files
author
Steve Canny
committed
txt: add Run.add_picture()
1 parent 6ac7ced commit d7001ed

File tree

4 files changed

+77
-3
lines changed

4 files changed

+77
-3
lines changed

docx/parts/document.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def add_picture(self, image_descriptor, run):
206206
@property
207207
def _inline_lst(self):
208208
body = self._body
209-
xpath = './w:p/w:r/w:drawing/wp:inline'
209+
xpath = '//w:p/w:r/w:drawing/wp:inline'
210210
return body.xpath(xpath)
211211

212212

docx/text.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,37 @@ def add_break(self, break_type=WD_BREAK.LINE):
196196
if clear is not None:
197197
br.clear = clear
198198

199+
def add_picture(self, image_path_or_stream, width=None, height=None):
200+
"""
201+
Return an |InlineShape| instance containing the image identified by
202+
*image_path_or_stream*, added to the end of this run.
203+
*image_path_or_stream* can be a path (a string) or a file-like object
204+
containing a binary image. If neither width nor height is specified,
205+
the picture appears at its native size. If only one is specified, it
206+
is used to compute a scaling factor that is then applied to the
207+
unspecified dimension, preserving the aspect ratio of the image. The
208+
native size of the picture is calculated using the dots-per-inch
209+
(dpi) value specified in the image file, defaulting to 72 dpi if no
210+
value is specified, as is often the case.
211+
"""
212+
inline_shapes = self.part.inline_shapes
213+
picture = inline_shapes.add_picture(image_path_or_stream, self)
214+
215+
# scale picture dimensions if width and/or height provided
216+
if width is not None or height is not None:
217+
native_width, native_height = picture.width, picture.height
218+
if width is None:
219+
scaling_factor = float(height) / float(native_height)
220+
width = int(round(native_width * scaling_factor))
221+
elif height is None:
222+
scaling_factor = float(width) / float(native_width)
223+
height = int(round(native_height * scaling_factor))
224+
# set picture to scaled dimensions
225+
picture.width = width
226+
picture.height = height
227+
228+
return picture
229+
199230
def add_tab(self):
200231
"""
201232
Add a ``<w:tab/>`` element at the end of the run, which Word

features/run-add-picture.feature

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ Feature: Add picture to a run
33
As a developer using python-docx
44
I need a way to add a picture to a run
55

6-
@wip
76
Scenario: Add a picture to a body paragraph run
87
Given a run
98
When I add a picture to the run
109
Then the picture appears at the end of the run
1110
And the document contains the inline picture
1211

1312

14-
@wip
1513
Scenario Outline: Add a picture to a run in a table cell
1614
Given a run inside a table cell retrieved from <cell-source>
1715
When I add a picture to the run

tests/test_text.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_UNDERLINE
1212
from docx.oxml.ns import qn
1313
from docx.oxml.text import CT_P, CT_R
14+
from docx.parts.document import InlineShapes
15+
from docx.shape import InlineShape
1416
from docx.text import Paragraph, Run
1517

1618
import pytest
@@ -280,6 +282,17 @@ def it_can_add_a_tab(self, add_tab_fixture):
280282
run.add_tab()
281283
assert run._r.xml == expected_xml
282284

285+
def it_can_add_a_picture(self, add_picture_fixture):
286+
(run, image_descriptor_, width, height, inline_shapes_,
287+
expected_width, expected_height, picture_) = add_picture_fixture
288+
picture = run.add_picture(image_descriptor_, width, height)
289+
inline_shapes_.add_picture.assert_called_once_with(
290+
image_descriptor_, run
291+
)
292+
assert picture is picture_
293+
assert picture.width == expected_width
294+
assert picture.height == expected_height
295+
283296
def it_can_remove_its_content_but_keep_formatting(self, clear_fixture):
284297
run, expected_xml = clear_fixture
285298
_run = run.clear()
@@ -314,6 +327,24 @@ def add_break_fixture(self, request):
314327
expected_xml = xml(expected_cxml)
315328
return run, break_type, expected_xml
316329

330+
@pytest.fixture(params=[
331+
(None, None, 200, 100),
332+
(1000, 500, 1000, 500),
333+
(2000, None, 2000, 1000),
334+
(None, 2000, 4000, 2000),
335+
])
336+
def add_picture_fixture(
337+
self, request, paragraph_, inline_shapes_, picture_):
338+
width, height, expected_width, expected_height = request.param
339+
paragraph_.part.inline_shapes = inline_shapes_
340+
run = Run(None, paragraph_)
341+
image_descriptor_ = 'image_descriptor_'
342+
picture_.width, picture_.height = 200, 100
343+
return (
344+
run, image_descriptor_, width, height, inline_shapes_,
345+
expected_width, expected_height, picture_
346+
)
347+
317348
@pytest.fixture(params=[
318349
('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)'),
319350
])
@@ -529,6 +560,20 @@ def underline_raise_fixture(self, request):
529560

530561
# fixture components ---------------------------------------------
531562

563+
@pytest.fixture
564+
def inline_shapes_(self, request, picture_):
565+
inline_shapes_ = instance_mock(request, InlineShapes)
566+
inline_shapes_.add_picture.return_value = picture_
567+
return inline_shapes_
568+
569+
@pytest.fixture
570+
def paragraph_(self, request):
571+
return instance_mock(request, Paragraph)
572+
573+
@pytest.fixture
574+
def picture_(self, request):
575+
return instance_mock(request, InlineShape)
576+
532577
@pytest.fixture
533578
def Text_(self, request):
534579
return class_mock(request, 'docx.text.Text')

0 commit comments

Comments
 (0)