Skip to content

Commit 10e93e2

Browse files
author
Steve Canny
committed
shp: add InlineShapes.add_picture()
1 parent 031c8b5 commit 10e93e2

File tree

3 files changed

+140
-8
lines changed

3 files changed

+140
-8
lines changed

docx/parts.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ def __init__(self, partname, content_type, document_elm, package):
2323
)
2424
self._element = document_elm
2525

26+
def add_image(self, image_descriptor):
27+
"""
28+
Return an ``(image_part, rId)`` 2-tuple for the image identified by
29+
*image_descriptor*. *image_part* is an |Image| instance corresponding
30+
to the image, newly created if not already present in document. *rId*
31+
is the key for the relationship between this document part and the
32+
image part, reused if already present, newly created if not.
33+
"""
34+
raise NotImplementedError
35+
2636
@property
2737
def blob(self):
2838
return serialize_part_xml(self._element)
@@ -48,6 +58,15 @@ def load(partname, content_type, blob, package):
4858
document = _Document(partname, content_type, document_elm, package)
4959
return document
5060

61+
@property
62+
def next_id(self):
63+
"""
64+
The next available positive integer id value in this document. Gaps
65+
in id sequence are filled. The id attribute value is unique in the
66+
document, without regard to the element type it appears on.
67+
"""
68+
raise NotImplementedError
69+
5170

5271
class _Body(object):
5372
"""
@@ -100,6 +119,13 @@ def tables(self):
100119
return [Table(tbl) for tbl in self._body.tbl_lst]
101120

102121

122+
class Image(Part):
123+
"""
124+
An image part. Corresponds to the target part of a relationship with type
125+
RELATIONSHIP_TYPE.IMAGE.
126+
"""
127+
128+
103129
class InlineShape(object):
104130
"""
105131
Proxy for an ``<wp:inline>`` element, representing the container for an
@@ -109,6 +135,22 @@ def __init__(self, inline):
109135
super(InlineShape, self).__init__()
110136
self._inline = inline
111137

138+
@classmethod
139+
def new_picture(cls, r, image, rId, shape_id):
140+
"""
141+
Return a new |InlineShape| instance containing an inline picture
142+
placement of the image part *image* appended to run *r* and
143+
uniquely identified by *shape_id*.
144+
"""
145+
# width, height, filename = (
146+
# image.width, image.height, image.filename
147+
# )
148+
# pic = CT_Picture.new(filename, rId, width, height)
149+
# inline = CT_Inline.new_inline(width, height, shape_id, pic)
150+
# r.add_drawing(inline)
151+
# return cls(inline)
152+
raise NotImplementedError
153+
112154
@property
113155
def type(self):
114156
graphicData = self._inline.graphic.graphicData
@@ -151,12 +193,26 @@ def __iter__(self):
151193
def __len__(self):
152194
return len(self._inline_lst)
153195

154-
def add_picture(self, image_path_or_stream):
196+
def add_picture(self, image_descriptor):
197+
"""
198+
Add the image identified by *image_descriptor* to the document at its
199+
native size. The picture is placed inline in a new paragraph at the
200+
end of the document. *image_descriptor* can be a path (a string) or a
201+
file-like object containing a binary image.
202+
"""
203+
rId, image = self.part.add_image(image_descriptor)
204+
shape_id = self.part.next_id
205+
r = self._body.add_p().add_r()
206+
return InlineShape.new_picture(r, image, rId, shape_id)
207+
208+
@property
209+
def part(self):
155210
"""
156-
Add the image at *image_path_or_stream* to the document at its native
157-
size. The picture is placed inline in a new paragraph at the end of
158-
the document.
211+
The package part containing this object, a |_Document| instance in
212+
this case.
159213
"""
214+
# return self._parent.part
215+
raise NotImplementedError
160216

161217
@property
162218
def _inline_lst(self):

tests/test_parts.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
from mock import Mock
1212

1313
from docx.enum.shape import WD_INLINE_SHAPE
14-
from docx.oxml.parts import CT_Document
14+
from docx.oxml.parts import CT_Body, CT_Document
1515
from docx.oxml.shared import nsmap
16-
from docx.parts import _Body, _Document, InlineShape, InlineShapes
16+
from docx.oxml.text import CT_R
17+
from docx.parts import _Body, _Document, Image, InlineShape, InlineShapes
1718
from docx.table import Table
1819
from docx.text import Paragraph
1920

@@ -26,7 +27,7 @@
2627
)
2728
from .oxml.unitdata.text import a_p, a_sectPr, an_r
2829
from .unitutil import (
29-
function_mock, class_mock, initializer_mock, instance_mock
30+
function_mock, class_mock, initializer_mock, instance_mock, property_mock
3031
)
3132

3233

@@ -360,8 +361,57 @@ def it_raises_on_indexed_access_out_of_range(
360361
too_high = inline_shape_count
361362
inline_shapes[too_high]
362363

364+
def it_can_add_an_inline_picture_to_the_document(
365+
self, add_picture_fixture):
366+
(inline_shapes, image_descriptor_, document_, InlineShape_, r_,
367+
image_, rId_, shape_id_, new_picture_shape_) = add_picture_fixture
368+
picture_shape = inline_shapes.add_picture(image_descriptor_)
369+
document_.add_image.assert_called_once_with(image_descriptor_)
370+
InlineShape_.new_picture.assert_called_once_with(
371+
r_, image_, rId_, shape_id_
372+
)
373+
assert picture_shape is new_picture_shape_
374+
363375
# fixtures -------------------------------------------------------
364376

377+
@pytest.fixture
378+
def add_picture_fixture(
379+
self, request, body_, document_, image_descriptor_, InlineShape_,
380+
r_, image_, rId_, shape_id_, new_picture_shape_):
381+
inline_shapes = InlineShapes(body_)
382+
property_mock(request, InlineShapes, 'part', return_value=document_)
383+
return (
384+
inline_shapes, image_descriptor_, document_, InlineShape_, r_,
385+
image_, rId_, shape_id_, new_picture_shape_
386+
)
387+
388+
@pytest.fixture
389+
def body_(self, request, r_):
390+
body_ = instance_mock(request, CT_Body)
391+
body_.add_p.return_value.add_r.return_value = r_
392+
return body_
393+
394+
@pytest.fixture
395+
def document_(self, request, rId_, image_, shape_id_):
396+
document_ = instance_mock(request, _Document, name='document_')
397+
document_.add_image.return_value = rId_, image_
398+
document_.next_id = shape_id_
399+
return document_
400+
401+
@pytest.fixture
402+
def image_(self, request):
403+
return instance_mock(request, Image)
404+
405+
@pytest.fixture
406+
def image_descriptor_(self, request):
407+
return instance_mock(request, str)
408+
409+
@pytest.fixture
410+
def InlineShape_(self, request, new_picture_shape_):
411+
InlineShape_ = class_mock(request, 'docx.parts.InlineShape')
412+
InlineShape_.new_picture.return_value = new_picture_shape_
413+
return InlineShape_
414+
365415
@pytest.fixture
366416
def inline_shapes_fixture(self):
367417
inline_shape_count = 2
@@ -380,3 +430,19 @@ def inline_shapes_fixture(self):
380430
).element
381431
inline_shapes = InlineShapes(body)
382432
return inline_shapes, inline_shape_count
433+
434+
@pytest.fixture
435+
def new_picture_shape_(self, request):
436+
return instance_mock(request, InlineShape)
437+
438+
@pytest.fixture
439+
def r_(self, request):
440+
return instance_mock(request, CT_R)
441+
442+
@pytest.fixture
443+
def rId_(self, request):
444+
return instance_mock(request, str)
445+
446+
@pytest.fixture
447+
def shape_id_(self, request):
448+
return instance_mock(request, int)

tests/unitutil.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import os
88

9-
from mock import create_autospec, Mock, patch
9+
from mock import create_autospec, Mock, patch, PropertyMock
1010

1111
from docx.oxml.shared import serialize_for_reading
1212

@@ -106,6 +106,16 @@ def method_mock(request, cls, method_name, **kwargs):
106106
return _patch.start()
107107

108108

109+
def property_mock(request, cls, prop_name, **kwargs):
110+
"""
111+
Return a mock for property *prop_name* on class *cls* where the patch is
112+
reversed after pytest uses it.
113+
"""
114+
_patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs)
115+
request.addfinalizer(_patch.stop)
116+
return _patch.start()
117+
118+
109119
def var_mock(request, q_var_name, **kwargs):
110120
"""
111121
Return a mock patching the variable with qualified name *q_var_name*.

0 commit comments

Comments
 (0)