Skip to content

Commit 8a0d893

Browse files
author
Steve Canny
committed
img: add ImagePart.default_cx, cy
Along the way: * transplanted _BaseLength and Inches, Emu, etc. from python-pptx * renamed width and height to cx, cy in a couple related spots
1 parent 3272199 commit 8a0d893

File tree

6 files changed

+127
-30
lines changed

6 files changed

+127
-30
lines changed

docx/oxml/shape.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def graphic(self):
8585
return self.find(qn('a:graphic'))
8686

8787
@classmethod
88-
def new(cls, width, height, shape_id, pic):
88+
def new(cls, cx, cy, shape_id, pic):
8989
"""
9090
Return a new ``<wp:inline>`` element populated with the values passed
9191
as parameters.
@@ -94,7 +94,7 @@ def new(cls, width, height, shape_id, pic):
9494
uri = nsmap['pic']
9595

9696
inline = OxmlElement('wp:inline', nsmap=nspfxmap('wp', 'r'))
97-
inline.append(CT_PositiveSize2D.new('wp:extent', width, height))
97+
inline.append(CT_PositiveSize2D.new('wp:extent', cx, cy))
9898
inline.append(CT_NonVisualDrawingProps.new(
9999
'wp:docPr', shape_id, name
100100
))

docx/parts/document.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,12 @@ def new_picture(cls, r, image_part, rId, shape_id):
153153
placement of *image_part* appended to run *r* and uniquely identified
154154
by *shape_id*.
155155
"""
156-
width, height, filename = (
157-
image_part.width, image_part.height, image_part.filename
156+
cx, cy, filename = (
157+
image_part.default_cx, image_part.default_cy, image_part.filename
158158
)
159159
pic_id = 0
160-
pic = CT_Picture.new(pic_id, filename, rId, width, height)
161-
inline = CT_Inline.new(width, height, shape_id, pic)
160+
pic = CT_Picture.new(pic_id, filename, rId, cx, cy)
161+
inline = CT_Inline.new(cx, cy, shape_id, pic)
162162
r.add_drawing(inline)
163163
return cls(inline)
164164

docx/parts/image.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
The proxy class for an image part, and related objects.
55
"""
66

7-
from __future__ import absolute_import, print_function, unicode_literals
7+
from __future__ import (
8+
absolute_import, division, print_function, unicode_literals
9+
)
810

911
import hashlib
1012
import os
@@ -16,7 +18,7 @@
1618

1719
from docx.opc.constants import CONTENT_TYPE as CT
1820
from docx.opc.package import Part
19-
from docx.shared import lazyproperty
21+
from docx.shared import Emu, Inches, lazyproperty
2022

2123

2224
class Image(object):
@@ -200,6 +202,28 @@ def __init__(self, partname, content_type, blob, image=None):
200202
super(ImagePart, self).__init__(partname, content_type, blob)
201203
self._image = image
202204

205+
@property
206+
def default_cx(self):
207+
"""
208+
Native width of this image, calculated from its width in pixels and
209+
horizontal dots per inch (dpi).
210+
"""
211+
px_width = self._image.px_width
212+
horz_dpi = self._image.horz_dpi
213+
width_in_inches = px_width / horz_dpi
214+
return Inches(width_in_inches)
215+
216+
@property
217+
def default_cy(self):
218+
"""
219+
Native height of this image, calculated from its height in pixels and
220+
vertical dots per inch (dpi).
221+
"""
222+
px_height = self._image.px_height
223+
horz_dpi = self._image.horz_dpi
224+
height_in_emu = 914400 * px_height / horz_dpi
225+
return Emu(height_in_emu)
226+
203227
@property
204228
def filename(self):
205229
"""
@@ -219,16 +243,6 @@ def from_image(cls, image, partname):
219243
"""
220244
return ImagePart(partname, image.content_type, image.blob, image)
221245

222-
@property
223-
def height(self):
224-
"""
225-
Native height of this image, calculated from its height in pixels and
226-
vertical dots per inch (dpi) when available. Default values are
227-
silently substituted when specific values cannot be parsed from the
228-
binary image byte stream.
229-
"""
230-
raise NotImplementedError
231-
232246
@classmethod
233247
def load(cls, partname, content_type, blob, package):
234248
"""
@@ -244,13 +258,3 @@ def sha1(self):
244258
SHA1 hash digest of the blob of this image part.
245259
"""
246260
raise NotImplementedError
247-
248-
@property
249-
def width(self):
250-
"""
251-
Native width of this image, calculated from its width in pixels and
252-
horizontal dots per inch (dpi) when available. Default values are
253-
silently substituted when specific values cannot be parsed from the
254-
binary image byte stream.
255-
"""
256-
raise NotImplementedError

docx/shared.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,84 @@
77
from __future__ import absolute_import, print_function, unicode_literals
88

99

10+
class _BaseLength(int):
11+
"""
12+
Base class for length classes Inches, Cm, Mm, Px, and Emu
13+
"""
14+
_EMUS_PER_INCH = 914400
15+
_EMUS_PER_CM = 360000
16+
_EMUS_PER_MM = 36000
17+
_EMUS_PER_PX = 12700
18+
19+
def __new__(cls, emu):
20+
return int.__new__(cls, emu)
21+
22+
@property
23+
def inches(self):
24+
return self / float(self._EMUS_PER_INCH)
25+
26+
@property
27+
def cm(self):
28+
return self / float(self._EMUS_PER_CM)
29+
30+
@property
31+
def mm(self):
32+
return self / float(self._EMUS_PER_MM)
33+
34+
@property
35+
def px(self):
36+
# round can somtimes return values like x.999999 which are truncated
37+
# to x by int(); adding the 0.1 prevents this
38+
return int(round(self / float(self._EMUS_PER_PX)) + 0.1)
39+
40+
@property
41+
def emu(self):
42+
return self
43+
44+
45+
class Inches(_BaseLength):
46+
"""Convenience constructor for length in inches."""
47+
def __new__(cls, inches):
48+
emu = int(inches * _BaseLength._EMUS_PER_INCH)
49+
return _BaseLength.__new__(cls, emu)
50+
51+
52+
class Cm(_BaseLength):
53+
"""Convenience constructor for length in centimeters."""
54+
def __new__(cls, cm):
55+
emu = int(cm * _BaseLength._EMUS_PER_CM)
56+
return _BaseLength.__new__(cls, emu)
57+
58+
59+
class Emu(_BaseLength):
60+
"""Convenience constructor for length in english metric units."""
61+
def __new__(cls, emu):
62+
return _BaseLength.__new__(cls, int(emu))
63+
64+
65+
class Mm(_BaseLength):
66+
"""Convenience constructor for length in millimeters."""
67+
def __new__(cls, mm):
68+
emu = int(mm * _BaseLength._EMUS_PER_MM)
69+
return _BaseLength.__new__(cls, emu)
70+
71+
72+
class Pt(int):
73+
"""Convenience class for setting font sizes in points"""
74+
_UNITS_PER_POINT = 100
75+
76+
def __new__(cls, pts):
77+
units = int(pts * Pt._UNITS_PER_POINT)
78+
return int.__new__(cls, units)
79+
80+
81+
class Px(_BaseLength):
82+
"""Convenience constructor for length in pixels."""
83+
def __new__(cls, px):
84+
emu = int(px * _BaseLength._EMUS_PER_PX)
85+
return _BaseLength.__new__(cls, emu)
86+
87+
1088
def lazyproperty(f):
1189
"""
1290
@lazyprop decorator. Decorated method will be called only on first access

tests/parts/test_document.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,8 @@ def image_params(self):
422422
def image_part_(self, request, image_params):
423423
filename, rId, cx, cy = image_params
424424
image_part_ = instance_mock(request, ImagePart)
425-
image_part_.width = str(cx)
426-
image_part_.height = str(cy)
425+
image_part_.default_cx = cx
426+
image_part_.default_cy = cy
427427
image_part_.filename = filename
428428
return image_part_
429429

tests/parts/test_image.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,27 @@ def it_can_construct_from_an_Image_instance(self, from_image_fixture):
107107
)
108108
assert isinstance(image_part, ImagePart)
109109

110+
def it_knows_its_default_dimensions_in_EMU(self, dimensions_fixture):
111+
image_part, cx, cy = dimensions_fixture
112+
assert image_part.default_cx == cx
113+
assert image_part.default_cy == cy
114+
110115
# fixtures -------------------------------------------------------
111116

112117
@pytest.fixture
113118
def blob_(self, request):
114119
return instance_mock(request, str)
115120

121+
# param for one known from test_files at 72 dpi and created with from_image
122+
# param for one loaded by PartFactory with no Image instance
123+
@pytest.fixture
124+
def dimensions_fixture(self):
125+
image_file_path = test_file('monty-truth.png')
126+
image = Image.load(image_file_path)
127+
image_part = ImagePart.from_image(image, None)
128+
cx, cy = 1905000, 2717800
129+
return image_part, cx, cy
130+
116131
@pytest.fixture
117132
def from_image_fixture(self, image_, partname_, ImagePart__init__):
118133
return image_, partname_, ImagePart__init__

0 commit comments

Comments
 (0)