Skip to content

Commit 086f219

Browse files
author
Steve Canny
committed
api: add Document.add_picture()
1 parent 3975b40 commit 086f219

File tree

6 files changed

+166
-32
lines changed

6 files changed

+166
-32
lines changed

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@
112112
.. |Table| replace:: :class:`Table`
113113
114114
.. |Text| replace:: :class:`Text`
115+
116+
.. |ValueError| replace:: :class:`ValueError`
115117
"""
116118

117119

docx/api.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
OpcPackage graph.
77
"""
88

9+
from __future__ import absolute_import, division, print_function
10+
911
import os
1012

1113
from docx.opc.constants import CONTENT_TYPE as CT
@@ -29,14 +31,6 @@ def __init__(self, docx=None):
2931
self._document_part = document_part
3032
self._package = package
3133

32-
def add_inline_picture(self, image_path_or_stream):
33-
"""
34-
Add the image at *image_path_or_stream* to the document at its native
35-
size. The picture is placed inline in a new paragraph at the end of
36-
the document.
37-
"""
38-
return self.inline_shapes.add_picture(image_path_or_stream)
39-
4034
def add_heading(self, text='', level=1):
4135
"""
4236
Return a heading paragraph newly added to the end of the document,
@@ -64,6 +58,34 @@ def add_paragraph(self, text='', style=None):
6458
p.style = style
6559
return p
6660

61+
def add_picture(self, image_path_or_stream, width=None, height=None):
62+
"""
63+
Add the image at *image_path_or_stream* in a new paragraph at the end
64+
of the document. If neither width nor height is specified, the
65+
picture appears at its native size. If only one is specified, it is
66+
used to compute a scaling factor that is then applied to the
67+
unspecified dimension, preserving the aspect ratio of the image. The
68+
native size of the picture is calculated using the dots-per-inch
69+
(dpi) value specified in the image file, defaulting to 72 dpi if no
70+
value is specified, as is often the case.
71+
"""
72+
picture = self.inline_shapes.add_picture(image_path_or_stream)
73+
74+
# scale picture dimensions if width and/or height provided
75+
if width is not None or height is not None:
76+
native_width, native_height = picture.width, picture.height
77+
if width is None:
78+
scaling_factor = float(height) / float(native_height)
79+
width = int(round(native_width * scaling_factor))
80+
elif height is None:
81+
scaling_factor = float(width) / float(native_width)
82+
height = int(round(native_height * scaling_factor))
83+
# set picture to scaled dimensions
84+
picture.width = width
85+
picture.height = height
86+
87+
return picture
88+
6789
@property
6890
def body(self):
6991
"""

features/api-add-picture.feature

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Feature: Append an inline picture on its own paragraph
2+
In order add an image to a document
3+
As a programmer using the basic python-docx API
4+
I need a method that adds a picture in its own paragraph
5+
6+
Scenario: Add a picture at native size
7+
Given a document
8+
When I add a picture specifying only the image file
9+
Then the document contains the inline picture
10+
And the picture has its native width and height
11+
12+
Scenario: Add a picture specifying both width and height
13+
Given a document
14+
When I add a picture specifying 1.75" width and 2.5" height
15+
Then the picture width is 1.75 inches
16+
And the picture height is 2.5 inches
17+
18+
Scenario: Add a picture specifying only width
19+
Given a document
20+
When I add a picture specifying a width of 1.5 inches
21+
Then the picture height is 2.14 inches
22+
23+
Scenario: Add a picture specifying only height
24+
Given a document
25+
When I add a picture specifying a height of 1.5 inches
26+
Then the picture width is 1.05 inches

features/steps/api.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
from behave import then, when
88

9+
from docx.shared import Inches
10+
11+
from .helpers import test_file_path
12+
913

1014
# when ====================================================
1115

@@ -43,6 +47,37 @@ def when_add_paragraph_without_specifying_text_or_style(context):
4347
document.add_paragraph()
4448

4549

50+
@when('I add a picture specifying 1.75" width and 2.5" height')
51+
def when_add_picture_specifying_width_and_height(context):
52+
document = context.document
53+
context.picture = document.add_picture(
54+
test_file_path('monty-truth.png'),
55+
width=Inches(1.75), height=Inches(2.5)
56+
)
57+
58+
59+
@when('I add a picture specifying a height of 1.5 inches')
60+
def when_add_picture_specifying_height(context):
61+
document = context.document
62+
context.picture = document.add_picture(
63+
test_file_path('monty-truth.png'), height=Inches(1.5)
64+
)
65+
66+
67+
@when('I add a picture specifying a width of 1.5 inches')
68+
def when_add_picture_specifying_width(context):
69+
document = context.document
70+
context.picture = document.add_picture(
71+
test_file_path('monty-truth.png'), width=Inches(1.5)
72+
)
73+
74+
75+
@when('I add a picture specifying only the image file')
76+
def when_add_picture_specifying_only_image_file(context):
77+
document = context.document
78+
context.picture = document.add_picture(test_file_path('monty-truth.png'))
79+
80+
4681
# then =====================================================
4782

4883
@then('the last paragraph contains the heading text')
@@ -76,6 +111,37 @@ def then_last_p_is_empty_paragraph_added(context):
76111
assert p.text == ''
77112

78113

114+
@then('the picture has its native width and height')
115+
def then_picture_has_native_width_and_height(context):
116+
picture = context.picture
117+
assert picture.width == 1905000, 'got %d' % picture.width
118+
assert picture.height == 2717800, 'got %d' % picture.height
119+
120+
121+
@then('the picture height is 2.14 inches')
122+
def then_picture_height_is_value_2(context):
123+
picture = context.picture
124+
assert picture.height == 1956816, 'got %d' % picture.height
125+
126+
127+
@then('the picture height is 2.5 inches')
128+
def then_picture_height_is_value(context):
129+
picture = context.picture
130+
assert picture.height == 2286000, 'got %d' % picture.height
131+
132+
133+
@then('the picture width is 1.05 inches')
134+
def then_picture_width_is_value_2(context):
135+
picture = context.picture
136+
assert picture.width == 961402, 'got %d' % picture.width
137+
138+
139+
@then('the picture width is 1.75 inches')
140+
def then_picture_width_is_value(context):
141+
picture = context.picture
142+
assert picture.width == 1600200, 'got %d' % picture.width
143+
144+
79145
@then('the style of the last paragraph is \'{style}\'')
80146
def then_style_of_last_paragraph_is_style(context, style):
81147
document = context.document

features/steps/shape.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,15 @@ def given_inline_shape_known_to_be_shape_of_type(context, shp_of_type):
5959
def when_add_inline_picture_from_file_like_object(context):
6060
document = context.document
6161
with open(test_file_path('monty-truth.png')) as f:
62-
context.inline_shape = (
63-
document.add_inline_picture(f)
64-
)
62+
context.inline_shape = document.inline_shapes.add_picture(f)
6563

6664

6765
@when('I add an inline picture to the document')
6866
def when_add_inline_picture_to_document(context):
6967
document = context.document
70-
context.inline_shape = (
71-
document.add_inline_picture(test_file_path('monty-truth.png'))
72-
)
68+
context.inline_shape = (document.inline_shapes.add_picture(
69+
test_file_path('monty-truth.png')
70+
))
7371

7472

7573
@when('I change the dimensions of the inline shape')

tests/test_api.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
from docx.parts.document import DocumentPart, InlineShapes
1313
from docx.text import Paragraph, Run
1414

15-
from .unitutil import class_mock, instance_mock, method_mock, var_mock
15+
from .unitutil import (
16+
instance_mock, class_mock, method_mock, property_mock, var_mock
17+
)
1618

1719

1820
class DescribeDocument(object):
@@ -69,6 +71,15 @@ def it_can_add_a_styled_paragraph(self, add_styled_paragraph_fixture):
6971
p = document.add_paragraph(style=style)
7072
assert p.style == style
7173

74+
def it_can_add_a_picture(self, add_picture_fixture):
75+
(document, image_path, width, height, inline_shapes_, expected_width,
76+
expected_height, picture_) = add_picture_fixture
77+
picture = document.add_picture(image_path, width, height)
78+
inline_shapes_.add_picture.assert_called_once_with(image_path)
79+
assert picture.width == expected_width
80+
assert picture.height == expected_height
81+
assert picture is picture_
82+
7283
def it_provides_access_to_the_document_body(self, document):
7384
body = document.body
7485
assert body is document._document_part.body
@@ -83,16 +94,6 @@ def it_provides_access_to_the_document_paragraphs(
8394
paragraphs = document.paragraphs
8495
assert paragraphs is paragraphs_
8596

86-
def it_can_add_an_inline_picture(self, add_picture_fixture):
87-
document, inline_shapes, image_path_or_stream_, inline_picture_ = (
88-
add_picture_fixture
89-
)
90-
inline_picture = document.add_inline_picture(image_path_or_stream_)
91-
inline_shapes.add_picture.assert_called_once_with(
92-
image_path_or_stream_
93-
)
94-
assert inline_picture is inline_picture_
95-
9697
def it_can_save_the_package(self, save_fixture):
9798
document, package_, file_ = save_fixture
9899
document.save(file_)
@@ -117,14 +118,23 @@ def add_paragraph_(self, request, p_):
117118
request, Document, 'add_paragraph', return_value=p_
118119
)
119120

120-
@pytest.fixture
121-
def add_picture_fixture(self, request, open_, document_part_):
121+
@pytest.fixture(params=[
122+
(None, None, 200, 100),
123+
(1000, 500, 1000, 500),
124+
(2000, None, 2000, 1000),
125+
(None, 2000, 4000, 2000),
126+
])
127+
def add_picture_fixture(
128+
self, request, Document_inline_shapes_, inline_shapes_):
129+
width, height, expected_width, expected_height = request.param
122130
document = Document()
123-
inline_shapes = instance_mock(request, InlineShapes)
124-
document_part_.inline_shapes = inline_shapes
125-
image_path_ = instance_mock(request, str)
126-
picture_shape_ = inline_shapes.add_picture.return_value
127-
return document, inline_shapes, image_path_, picture_shape_
131+
image_path_ = instance_mock(request, str, name='image_path_')
132+
picture_ = inline_shapes_.add_picture.return_value
133+
picture_.width, picture_.height = 200, 100
134+
return (
135+
document, image_path_, width, height, inline_shapes_,
136+
expected_width, expected_height, picture_
137+
)
128138

129139
@pytest.fixture
130140
def add_styled_paragraph_fixture(self, document, p_):
@@ -140,6 +150,12 @@ def add_text_paragraph_fixture(self, document, p_, r_):
140150
def default_docx_(self, request):
141151
return var_mock(request, 'docx.api._default_docx_path')
142152

153+
@pytest.fixture
154+
def Document_inline_shapes_(self, request, inline_shapes_):
155+
return property_mock(
156+
request, Document, 'inline_shapes', return_value=inline_shapes_
157+
)
158+
143159
@pytest.fixture
144160
def document(self, open_):
145161
return Document()
@@ -161,6 +177,10 @@ def docx_(self, request):
161177
def init_fixture(self, docx_, open_):
162178
return docx_, open_
163179

180+
@pytest.fixture
181+
def inline_shapes_(self, request):
182+
return instance_mock(request, InlineShapes)
183+
164184
@pytest.fixture
165185
def open_(self, request, document_part_, package_):
166186
return method_mock(

0 commit comments

Comments
 (0)