Skip to content

Commit d89eac6

Browse files
author
Steve Canny
committed
refac: introduce BaseImageHeader base class
1 parent 2a9a8b8 commit d89eac6

File tree

8 files changed

+116
-91
lines changed

8 files changed

+116
-91
lines changed

docx/image/image.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ class Image(object):
1717
Graphical image stream such as JPEG, PNG, or GIF with properties and
1818
methods required by ImagePart.
1919
"""
20-
def __init__(self, blob, filename, px_width, px_height, attrs):
20+
def __init__(self, blob, filename, image_header):
2121
super(Image, self).__init__()
2222
self._blob = blob
2323
self._filename = filename
24-
self._px_width = px_width
25-
self._px_height = px_height
26-
self._attrs = attrs
24+
self._image_header = image_header
2725

2826
@classmethod
2927
def from_file(cls, image_descriptor):
@@ -64,7 +62,54 @@ def _from_stream(cls, stream, blob, filename=None):
6462
Return an instance of the |Image| subclass corresponding to the
6563
format of the image in *stream*.
6664
"""
67-
# import at execution time to avoid circular import
68-
from docx.image import image_cls_that_can_parse
69-
ImageSubclass = image_cls_that_can_parse(stream)
70-
return ImageSubclass.from_stream(stream, blob, filename)
65+
image_header = _ImageHeaderFactory(stream)
66+
return cls(blob, filename, image_header)
67+
68+
69+
def _ImageHeaderFactory(stream):
70+
"""
71+
Return a |BaseImageHeader| subclass instance that knows how to parse the
72+
headers of the image in *stream*.
73+
"""
74+
raise NotImplementedError
75+
76+
77+
class BaseImageHeader(object):
78+
"""
79+
Base class for image header subclasses like |Jpeg| and |Tiff|.
80+
"""
81+
def __init__(self, px_width, px_height, horz_dpi, vert_dpi):
82+
self._px_width = px_width
83+
self._px_height = px_height
84+
self._horz_dpi = horz_dpi
85+
self._vert_dpi = vert_dpi
86+
87+
@property
88+
def px_width(self):
89+
"""
90+
The horizontal pixel dimension of the image
91+
"""
92+
return self._px_width
93+
94+
@property
95+
def px_height(self):
96+
"""
97+
The vertical pixel dimension of the image
98+
"""
99+
return self._px_height
100+
101+
@property
102+
def horz_dpi(self):
103+
"""
104+
Integer dots per inch for the width of this image. Defaults to 72
105+
when not present in the file, as is often the case.
106+
"""
107+
return self._horz_dpi
108+
109+
@property
110+
def vert_dpi(self):
111+
"""
112+
Integer dots per inch for the height of this image. Defaults to 72
113+
when not present in the file, as is often the case.
114+
"""
115+
return self._vert_dpi

docx/image/jpeg.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,14 @@
1010
from ..compat import BytesIO
1111
from .constants import JPEG_MARKER_CODE, MIME_TYPE
1212
from .helpers import BIG_ENDIAN, StreamReader
13-
from .image import Image
13+
from .image import BaseImageHeader
1414
from .tiff import Tiff
1515

1616

17-
class Jpeg(Image):
17+
class Jpeg(BaseImageHeader):
1818
"""
1919
Base class for JFIF and EXIF subclasses.
2020
"""
21-
def __init__(self, blob, filename, cx, cy, horz_dpi, vert_dpi):
22-
super(Jpeg, self).__init__(blob, filename, cx, cy, attrs={})
23-
self._horz_dpi = horz_dpi
24-
self._vert_dpi = vert_dpi
25-
2621
@property
2722
def content_type(self):
2823
"""
@@ -31,22 +26,6 @@ def content_type(self):
3126
"""
3227
return MIME_TYPE.JPEG
3328

34-
@property
35-
def horz_dpi(self):
36-
"""
37-
Integer dots per inch for the width of this image. Defaults to 72
38-
when not present in the file, as is often the case.
39-
"""
40-
return self._horz_dpi
41-
42-
@property
43-
def vert_dpi(self):
44-
"""
45-
Integer dots per inch for the height of this image. Defaults to 72
46-
when not present in the file, as is often the case.
47-
"""
48-
return self._vert_dpi
49-
5029

5130
class Exif(Jpeg):
5231
"""
@@ -66,7 +45,7 @@ def from_stream(cls, stream, blob, filename):
6645
horz_dpi = markers.app1.horz_dpi
6746
vert_dpi = markers.app1.vert_dpi
6847

69-
return cls(blob, filename, px_width, px_height, horz_dpi, vert_dpi)
48+
return cls(px_width, px_height, horz_dpi, vert_dpi)
7049

7150

7251
class Jfif(Jpeg):
@@ -80,10 +59,13 @@ def from_stream(cls, stream, blob, filename):
8059
in *stream*.
8160
"""
8261
markers = _JfifMarkers.from_stream(stream)
83-
sof, app0 = markers.sof, markers.app0
84-
cx, cy = sof.px_width, sof.px_height
85-
horz_dpi, vert_dpi = app0.horz_dpi, app0.vert_dpi
86-
return cls(blob, filename, cx, cy, horz_dpi, vert_dpi)
62+
63+
px_width = markers.sof.px_width
64+
px_height = markers.sof.px_height
65+
horz_dpi = markers.app0.horz_dpi
66+
vert_dpi = markers.app0.vert_dpi
67+
68+
return cls(px_width, px_height, horz_dpi, vert_dpi)
8769

8870

8971
class _JfifMarkers(object):

docx/image/png.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55
from .constants import MIME_TYPE, TAG
66
from .exceptions import InvalidImageStreamError
77
from .helpers import StreamReader
8-
from .image import Image
8+
from .image import BaseImageHeader
99

1010

1111
_CHUNK_TYPE_IHDR = 'IHDR'
1212
_CHUNK_TYPE_pHYs = 'pHYs'
1313
_CHUNK_TYPE_IEND = 'IEND'
1414

1515

16-
class Png(Image):
16+
class Png(BaseImageHeader):
1717
"""
1818
Image header parser for PNG images
1919
"""
2020
def __init__(self, blob, filename, cx, cy, attrs):
21-
super(Png, self).__init__(blob, filename, cx, cy, attrs)
21+
super(Png, self).__init__(cx, cy, None, None)
22+
self._attrs = attrs
2223

2324
@property
2425
def content_type(self):

docx/image/tiff.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@
44

55
from .constants import MIME_TYPE, TIFF_FLD, TIFF_TAG
66
from .helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader
7-
from .image import Image
7+
from .image import BaseImageHeader
88

99

10-
class Tiff(Image):
10+
class Tiff(BaseImageHeader):
1111
"""
1212
Image header parser for TIFF images. Handles both big and little endian
1313
byte ordering.
1414
"""
15-
def __init__(self, blob, filename, cx, cy, horz_dpi, vert_dpi):
16-
super(Tiff, self).__init__(blob, filename, cx, cy, {})
17-
self._horz_dpi = horz_dpi
18-
self._vert_dpi = vert_dpi
19-
2015
@property
2116
def content_type(self):
2217
"""
@@ -32,25 +27,13 @@ def from_stream(cls, stream, blob, filename):
3227
in *stream*.
3328
"""
3429
parser = _TiffParser.parse(stream)
30+
3531
px_width = parser.px_width
3632
px_height = parser.px_height
3733
horz_dpi = parser.horz_dpi
3834
vert_dpi = parser.vert_dpi
39-
return cls(blob, filename, px_width, px_height, horz_dpi, vert_dpi)
4035

41-
@property
42-
def horz_dpi(self):
43-
"""
44-
The intended print density of the image's pixels along the x-axis
45-
"""
46-
return self._horz_dpi
47-
48-
@property
49-
def vert_dpi(self):
50-
"""
51-
The intended print density of the image's pixels along the y-axis
52-
"""
53-
return self._vert_dpi
36+
return cls(px_width, px_height, horz_dpi, vert_dpi)
5437

5538

5639
class _TiffParser(object):

features/img-characterize-image.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Feature: Characterize an image file
33
As a programmer using the advanced python-docx API
44
I need a way to determine the image content type and size
55

6+
@wip
67
Scenario Outline: Characterize an image file
78
Given the image file '<filename>'
89
When I construct an image using the image path

tests/image/test_image.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
from docx.image.bmp import Bmp
1414
from docx.image.exceptions import UnrecognizedImageError
1515
from docx.image.gif import Gif
16-
from docx.image.image import Image
16+
from docx.image.image import BaseImageHeader, Image
1717
from docx.image.jpeg import Exif, Jfif
1818
from docx.image.png import Png
1919
from docx.image.tiff import Tiff
2020
from docx.opc.constants import CONTENT_TYPE as CT
2121

2222
from ..unitutil import (
23-
function_mock, class_mock, instance_mock, loose_mock, method_mock,
23+
function_mock, class_mock, initializer_mock, instance_mock, method_mock,
2424
test_file
2525
)
2626

@@ -75,14 +75,16 @@ def it_can_construct_from_an_image_file_like(self, from_filelike_fixture):
7575
assert image is image_
7676

7777
def it_can_construct_from_an_image_stream(self, from_stream_fixture):
78-
(stream_, blob_, filename_, image_, image_cls_that_can_parse_,
79-
image_cls_) = from_stream_fixture
78+
# fixture ----------------------
79+
stream_, blob_, filename_ = from_stream_fixture[:3]
80+
_ImageHeaderFactory_, image_header_ = from_stream_fixture[3:5]
81+
Image__init_ = from_stream_fixture[5]
82+
# exercise ---------------------
8083
image = Image._from_stream(stream_, blob_, filename_)
81-
image_cls_that_can_parse_.assert_called_once_with(stream_)
82-
image_cls_.from_stream.assert_called_once_with(
83-
stream_, blob_, filename_
84-
)
85-
assert image is image_
84+
# verify -----------------------
85+
_ImageHeaderFactory_.assert_called_once_with(stream_)
86+
Image__init_.assert_called_once_with(blob_, filename_, image_header_)
87+
assert isinstance(image, Image)
8688

8789
# fixtures -------------------------------------------------------
8890

@@ -118,11 +120,11 @@ def from_path_fixture(self, _from_stream_, BytesIO_, stream_, image_):
118120

119121
@pytest.fixture
120122
def from_stream_fixture(
121-
self, stream_, blob_, filename_, image_,
122-
image_cls_that_can_parse_, image_cls_):
123+
self, stream_, blob_, filename_, _ImageHeaderFactory_,
124+
image_header_, Image__init_):
123125
return (
124-
stream_, blob_, filename_, image_, image_cls_that_can_parse_,
125-
image_cls_
126+
stream_, blob_, filename_, _ImageHeaderFactory_, image_header_,
127+
Image__init_
126128
)
127129

128130
@pytest.fixture
@@ -136,23 +138,40 @@ def image_(self, request):
136138
return instance_mock(request, Image)
137139

138140
@pytest.fixture
139-
def image_cls_(self, request, image_):
140-
image_cls_ = loose_mock(request)
141-
image_cls_.from_stream.return_value = image_
142-
return image_cls_
143-
144-
@pytest.fixture
145-
def image_cls_that_can_parse_(self, request, image_cls_):
141+
def _ImageHeaderFactory_(self, request, image_header_):
146142
return function_mock(
147-
request, 'docx.image.image_cls_that_can_parse',
148-
return_value=image_cls_
143+
request, 'docx.image.image._ImageHeaderFactory',
144+
return_value=image_header_
149145
)
150146

147+
@pytest.fixture
148+
def image_header_(self, request):
149+
return instance_mock(request, BaseImageHeader)
150+
151+
@pytest.fixture
152+
def Image__init_(self, request):
153+
return initializer_mock(request, Image)
154+
151155
@pytest.fixture
152156
def stream_(self, request):
153157
return instance_mock(request, BytesIO)
154158

155159

160+
class DescribeBaseImageHeader(object):
161+
162+
def it_knows_the_image_dimensions(self):
163+
px_width, px_height = 42, 24
164+
image_header = BaseImageHeader(px_width, px_height, None, None)
165+
assert image_header.px_width == px_width
166+
assert image_header.px_height == px_height
167+
168+
def it_knows_the_horz_and_vert_dpi_of_the_image(self):
169+
horz_dpi, vert_dpi = 42, 24
170+
image_header = BaseImageHeader(None, None, horz_dpi, vert_dpi)
171+
assert image_header.horz_dpi == horz_dpi
172+
assert image_header.vert_dpi == vert_dpi
173+
174+
156175
class DescribeImage_OLD(object):
157176

158177
def it_can_construct_from_an_image_path(self):

tests/image/test_jpeg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class DescribeJpeg(object):
2828

2929
def it_knows_its_content_type(self):
30-
jpeg = Jpeg(None, None, None, None, None, None)
30+
jpeg = Jpeg(None, None, None, None)
3131
assert jpeg.content_type == MIME_TYPE.JPEG
3232

3333

tests/image/test_tiff.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,14 @@ def it_can_construct_from_a_tiff_stream(self, from_stream_fixture):
3232
tiff = Tiff.from_stream(stream_, blob_, filename_)
3333
_TiffParser_.parse.assert_called_once_with(stream_)
3434
Tiff__init_.assert_called_once_with(
35-
blob_, filename_, px_width, px_height, horz_dpi, vert_dpi
35+
px_width, px_height, horz_dpi, vert_dpi
3636
)
3737
assert isinstance(tiff, Tiff)
3838

3939
def it_knows_its_content_type(self):
40-
tiff = Tiff(None, None, None, None, None, None)
40+
tiff = Tiff(None, None, None, None)
4141
assert tiff.content_type == MIME_TYPE.TIFF
4242

43-
def it_knows_the_horz_and_vert_dpi_of_the_tiff_image(self):
44-
horz_dpi, vert_dpi = 42, 24
45-
tiff = Tiff(None, None, None, None, horz_dpi, vert_dpi)
46-
assert tiff.horz_dpi == horz_dpi
47-
assert tiff.vert_dpi == vert_dpi
48-
4943
# fixtures -------------------------------------------------------
5044

5145
@pytest.fixture

0 commit comments

Comments
 (0)