Skip to content

Commit 30fb4fa

Browse files
author
Steve Canny
committed
refac: introduce _ImageHeaderFactory()
* replaces image_cls_that_can_parse() * All image header classes now inherit from BaseImageHeader rather than Image * changes X_ImageHeader.from_stream() signature, removing blob and filename parameters * Image gets .content_type, horz_dpi, and vert_dpi as direct properties now they are no longer provided by subclasses
1 parent d89eac6 commit 30fb4fa

File tree

12 files changed

+180
-135
lines changed

12 files changed

+180
-135
lines changed

docx/image/__init__.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
from docx.compat import BytesIO, is_string
2121
from docx.image.bmp import Bmp
22-
from docx.image.exceptions import UnrecognizedImageError
2322
from docx.image.gif import Gif
2423
from docx.image.jpeg import Exif, Jfif
2524
from docx.image.png import Png
@@ -41,24 +40,6 @@
4140
)
4241

4342

44-
def image_cls_that_can_parse(stream):
45-
"""
46-
Return the |Image| subclass that can parse the headers of the image file
47-
contained in *stream*.
48-
"""
49-
def read_32(stream):
50-
stream.seek(0)
51-
return stream.read(32)
52-
53-
header = read_32(stream)
54-
for cls, offset, signature_bytes in SIGNATURES:
55-
end = offset + len(signature_bytes)
56-
found_bytes = header[offset:end]
57-
if found_bytes == signature_bytes:
58-
return cls
59-
raise UnrecognizedImageError
60-
61-
6243
class Image_OLD(object):
6344
"""
6445
A helper object that knows how to analyze an image file.

docx/image/bmp.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
# encoding: utf-8
22

3+
from __future__ import absolute_import, division, print_function
34

4-
class Bmp(object):
5+
from .image import BaseImageHeader
6+
7+
8+
class Bmp(BaseImageHeader):
59
"""
610
Image header parser for BMP images
711
"""
12+
@classmethod
13+
def from_stream(cls, stream):
14+
"""
15+
Return |Bmp| instance having header properties parsed from BMP image
16+
in *stream*.
17+
"""
18+
return cls(None, None, None, None)

docx/image/gif.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22

33
from __future__ import absolute_import, division, print_function
44

5-
from .image import Image
5+
from .image import BaseImageHeader
66

77

8-
class Gif(Image):
8+
class Gif(BaseImageHeader):
99
"""
1010
Image header parser for GIF images. Note that the GIF format does not
1111
support resolution (DPI) information. Both horizontal and vertical DPI
1212
default to 72.
1313
"""
14+
@classmethod
15+
def from_stream(cls, stream):
16+
"""
17+
Return |Gif| instance having header properties parsed from GIF image
18+
in *stream*.
19+
"""
20+
return cls(None, None, None, None)

docx/image/image.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
import os
1111

12-
from docx.compat import BytesIO, is_string
12+
from ..compat import BytesIO, is_string
13+
from .exceptions import UnrecognizedImageError
1314

1415

1516
class Image(object):
@@ -42,19 +43,43 @@ def from_file(cls, image_descriptor):
4243
filename = None
4344
return cls._from_stream(stream, blob, filename)
4445

46+
@property
47+
def content_type(self):
48+
"""
49+
MIME content type for this image, e.g. ``'image/jpeg'`` for a JPEG
50+
image
51+
"""
52+
return self._image_header.content_type
53+
4554
@property
4655
def px_width(self):
4756
"""
4857
The horizontal pixel dimension of the image
4958
"""
50-
return self._px_width
59+
return self._image_header.px_width
5160

5261
@property
5362
def px_height(self):
5463
"""
5564
The vertical pixel dimension of the image
5665
"""
57-
return self._px_height
66+
return self._image_header.px_height
67+
68+
@property
69+
def horz_dpi(self):
70+
"""
71+
Integer dots per inch for the width of this image. Defaults to 72
72+
when not present in the file, as is often the case.
73+
"""
74+
return self._image_header.horz_dpi
75+
76+
@property
77+
def vert_dpi(self):
78+
"""
79+
Integer dots per inch for the height of this image. Defaults to 72
80+
when not present in the file, as is often the case.
81+
"""
82+
return self._image_header.vert_dpi
5883

5984
@classmethod
6085
def _from_stream(cls, stream, blob, filename=None):
@@ -71,7 +96,19 @@ def _ImageHeaderFactory(stream):
7196
Return a |BaseImageHeader| subclass instance that knows how to parse the
7297
headers of the image in *stream*.
7398
"""
74-
raise NotImplementedError
99+
from docx.image import SIGNATURES
100+
101+
def read_32(stream):
102+
stream.seek(0)
103+
return stream.read(32)
104+
105+
header = read_32(stream)
106+
for cls, offset, signature_bytes in SIGNATURES:
107+
end = offset + len(signature_bytes)
108+
found_bytes = header[offset:end]
109+
if found_bytes == signature_bytes:
110+
return cls.from_stream(stream)
111+
raise UnrecognizedImageError
75112

76113

77114
class BaseImageHeader(object):
@@ -84,6 +121,16 @@ def __init__(self, px_width, px_height, horz_dpi, vert_dpi):
84121
self._horz_dpi = horz_dpi
85122
self._vert_dpi = vert_dpi
86123

124+
@property
125+
def content_type(self):
126+
"""
127+
Abstract property definition, must be implemented by all subclasses.
128+
"""
129+
raise NotImplementedError(
130+
'content_type property must be implemented by all subclasses of '
131+
'BaseImageHeader'
132+
)
133+
87134
@property
88135
def px_width(self):
89136
"""

docx/image/jpeg.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Exif(Jpeg):
3232
Image header parser for Exif image format
3333
"""
3434
@classmethod
35-
def from_stream(cls, stream, blob, filename):
35+
def from_stream(cls, stream):
3636
"""
3737
Return |Exif| instance having header properties parsed from Exif
3838
image in *stream*.
@@ -53,7 +53,7 @@ class Jfif(Jpeg):
5353
Image header parser for JFIF image format
5454
"""
5555
@classmethod
56-
def from_stream(cls, stream, blob, filename):
56+
def from_stream(cls, stream):
5757
"""
5858
Return a |Jfif| instance having header properties parsed from image
5959
in *stream*.
@@ -445,7 +445,7 @@ def _tiff_from_exif_segment(cls, stream, offset, segment_length):
445445
stream.seek(offset+8)
446446
segment_bytes = stream.read(segment_length-8)
447447
substream = BytesIO(segment_bytes)
448-
return Tiff.from_stream(substream, None, None)
448+
return Tiff.from_stream(substream)
449449

450450

451451
class _SofMarker(_Marker):

docx/image/png.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Png(BaseImageHeader):
1717
"""
1818
Image header parser for PNG images
1919
"""
20-
def __init__(self, blob, filename, cx, cy, attrs):
20+
def __init__(self, cx, cy, attrs):
2121
super(Png, self).__init__(cx, cy, None, None)
2222
self._attrs = attrs
2323

@@ -30,15 +30,15 @@ def content_type(self):
3030
return MIME_TYPE.PNG
3131

3232
@classmethod
33-
def from_stream(cls, stream, blob, filename):
33+
def from_stream(cls, stream):
3434
"""
3535
Return a |Png| instance having header properties parsed from image in
3636
*stream*.
3737
"""
3838
stream_rdr = StreamReader(stream, '>')
3939
attrs = cls._parse_png_headers(stream_rdr)
4040
cx, cy = attrs.pop('px_width'), attrs.pop('px_height')
41-
return Png(blob, filename, cx, cy, attrs)
41+
return Png(cx, cy, attrs)
4242

4343
@property
4444
def horz_dpi(self):

docx/image/tiff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def content_type(self):
2121
return MIME_TYPE.TIFF
2222

2323
@classmethod
24-
def from_stream(cls, stream, blob, filename):
24+
def from_stream(cls, stream):
2525
"""
2626
Return a |Tiff| instance containing the properties of the TIFF image
2727
in *stream*.

features/img-characterize-image.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ 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
76
Scenario Outline: Characterize an image file
87
Given the image file '<filename>'
98
When I construct an image using the image path

tests/image/test_image.py

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
import pytest
1010

1111
from docx.compat import BytesIO
12-
from docx.image import image_cls_that_can_parse, Image_OLD
12+
from docx.image import Image_OLD
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 BaseImageHeader, Image
16+
from docx.image.image import BaseImageHeader, Image, _ImageHeaderFactory
1717
from docx.image.jpeg import Exif, Jfif
1818
from docx.image.png import Png
1919
from docx.image.tiff import Tiff
@@ -25,39 +25,6 @@
2525
)
2626

2727

28-
class Describe_image_cls_that_can_parse(object):
29-
30-
def it_can_recognize_an_image_stream(self, image_cls_lookup_fixture):
31-
stream, expected_class = image_cls_lookup_fixture
32-
ImageSubclass = image_cls_that_can_parse(stream)
33-
assert ImageSubclass is expected_class
34-
35-
def it_raises_on_unrecognized_image_stream(self):
36-
stream = BytesIO(b'foobar 666 not an image stream')
37-
with pytest.raises(UnrecognizedImageError):
38-
image_cls_that_can_parse(stream)
39-
40-
# fixtures -------------------------------------------------------
41-
42-
@pytest.fixture(params=[
43-
('python-icon.png', Png),
44-
('python-icon.jpeg', Jfif),
45-
('exif-420-dpi.jpg', Exif),
46-
('sonic.gif', Gif),
47-
('72-dpi.tiff', Tiff),
48-
('little-endian.tif', Tiff),
49-
('python.bmp', Bmp),
50-
])
51-
def image_cls_lookup_fixture(self, request):
52-
image_filename, expected_class = request.param
53-
image_path = test_file(image_filename)
54-
with open(image_path, 'rb') as f:
55-
blob = f.read()
56-
image_stream = BytesIO(blob)
57-
image_stream.seek(666)
58-
return image_stream, expected_class
59-
60-
6128
class DescribeImage(object):
6229

6330
def it_can_construct_from_an_image_path(self, from_path_fixture):
@@ -86,6 +53,23 @@ def it_can_construct_from_an_image_stream(self, from_stream_fixture):
8653
Image__init_.assert_called_once_with(blob_, filename_, image_header_)
8754
assert isinstance(image, Image)
8855

56+
def it_knows_the_image_content_type(self, content_type_fixture):
57+
image_header_, content_type = content_type_fixture
58+
image = Image(None, None, image_header_)
59+
assert image.content_type == content_type
60+
61+
def it_knows_the_image_dimensions(self, dimensions_fixture):
62+
image_header_, px_width, px_height = dimensions_fixture
63+
image = Image(None, None, image_header_)
64+
assert image.px_width == px_width
65+
assert image.px_height == px_height
66+
67+
def it_knows_the_horz_and_vert_dpi_of_the_image(self, dpi_fixture):
68+
image_header_, horz_dpi, vert_dpi = dpi_fixture
69+
image = Image(None, None, image_header_)
70+
assert image.horz_dpi == horz_dpi
71+
assert image.vert_dpi == vert_dpi
72+
8973
# fixtures -------------------------------------------------------
9074

9175
@pytest.fixture
@@ -98,6 +82,26 @@ def BytesIO_(self, request, stream_):
9882
request, 'docx.image.image.BytesIO', return_value=stream_
9983
)
10084

85+
@pytest.fixture
86+
def content_type_fixture(self, image_header_):
87+
content_type = 'image/foobar'
88+
image_header_.content_type = content_type
89+
return image_header_, content_type
90+
91+
@pytest.fixture
92+
def dimensions_fixture(self, image_header_):
93+
px_width, px_height = 111, 222
94+
image_header_.px_width = px_width
95+
image_header_.px_height = px_height
96+
return image_header_, px_width, px_height
97+
98+
@pytest.fixture
99+
def dpi_fixture(self, image_header_):
100+
horz_dpi, vert_dpi = 333, 444
101+
image_header_.horz_dpi = horz_dpi
102+
image_header_.vert_dpi = vert_dpi
103+
return image_header_, horz_dpi, vert_dpi
104+
101105
@pytest.fixture
102106
def filename_(self, request):
103107
return instance_mock(request, str)
@@ -157,6 +161,40 @@ def stream_(self, request):
157161
return instance_mock(request, BytesIO)
158162

159163

164+
class Describe_ImageHeaderFactory(object):
165+
166+
def it_constructs_the_right_class_for_a_given_image_stream(
167+
self, call_fixture):
168+
stream, expected_class = call_fixture
169+
image_header = _ImageHeaderFactory(stream)
170+
assert isinstance(image_header, expected_class)
171+
172+
def it_raises_on_unrecognized_image_stream(self):
173+
stream = BytesIO(b'foobar 666 not an image stream')
174+
with pytest.raises(UnrecognizedImageError):
175+
_ImageHeaderFactory(stream)
176+
177+
# fixtures -------------------------------------------------------
178+
179+
@pytest.fixture(params=[
180+
('python-icon.png', Png),
181+
('python-icon.jpeg', Jfif),
182+
('exif-420-dpi.jpg', Exif),
183+
('sonic.gif', Gif),
184+
('72-dpi.tiff', Tiff),
185+
('little-endian.tif', Tiff),
186+
('python.bmp', Bmp),
187+
])
188+
def call_fixture(self, request):
189+
image_filename, expected_class = request.param
190+
image_path = test_file(image_filename)
191+
with open(image_path, 'rb') as f:
192+
blob = f.read()
193+
image_stream = BytesIO(blob)
194+
image_stream.seek(666)
195+
return image_stream, expected_class
196+
197+
160198
class DescribeBaseImageHeader(object):
161199

162200
def it_knows_the_image_dimensions(self):

0 commit comments

Comments
 (0)