Skip to content

Commit bd91474

Browse files
author
Steve Canny
committed
img: add Exif.from_stream()
* migrate Jfif.__init__(), .horz_dpi, and .vert_dpi to Jpeg superclass * add _JfifMarkers.__str__() to support debugging
1 parent a47e0aa commit bd91474

3 files changed

Lines changed: 125 additions & 24 deletions

File tree

docx/image/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class JPEG_MARKER_CODE(object):
7272
SOFE, SOFF
7373
)
7474

75-
names = {
75+
marker_names = {
7676
b'\x00': 'UNKNOWN',
7777
b'\xC0': 'SOF0',
7878
b'\xC2': 'SOF2',

docx/image/jpeg.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class Jpeg(Image):
1616
"""
1717
Base class for JFIF and EXIF subclasses.
1818
"""
19+
def __init__(self, blob, filename, cx, cy, horz_dpi, vert_dpi):
20+
super(Jpeg, self).__init__(blob, filename, cx, cy, attrs={})
21+
self._horz_dpi = horz_dpi
22+
self._vert_dpi = vert_dpi
23+
1924
@property
2025
def content_type(self):
2126
"""
@@ -24,22 +29,48 @@ def content_type(self):
2429
"""
2530
return MIME_TYPE.JPEG
2631

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

2849
class Exif(Jpeg):
2950
"""
3051
Image header parser for Exif image format
3152
"""
53+
@classmethod
54+
def from_stream(cls, stream, blob, filename):
55+
"""
56+
Return |Exif| instance having header properties parsed from Exif
57+
image in *stream*.
58+
"""
59+
markers = _JfifMarkers.from_stream(stream)
60+
# print('\n%s' % markers)
61+
62+
px_width = markers.sof.px_width
63+
px_height = markers.sof.px_height
64+
horz_dpi = markers.app1.horz_dpi
65+
vert_dpi = markers.app1.vert_dpi
66+
67+
return cls(blob, filename, px_width, px_height, horz_dpi, vert_dpi)
3268

3369

3470
class Jfif(Jpeg):
3571
"""
3672
Image header parser for JFIF image format
3773
"""
38-
def __init__(self, blob, filename, cx, cy, horz_dpi, vert_dpi):
39-
super(Jfif, self).__init__(blob, filename, cx, cy, attrs={})
40-
self._horz_dpi = horz_dpi
41-
self._vert_dpi = vert_dpi
42-
4374
@classmethod
4475
def from_stream(cls, stream, blob, filename):
4576
"""
@@ -52,22 +83,6 @@ def from_stream(cls, stream, blob, filename):
5283
horz_dpi, vert_dpi = app0.horz_dpi, app0.vert_dpi
5384
return cls(blob, filename, cx, cy, horz_dpi, vert_dpi)
5485

55-
@property
56-
def horz_dpi(self):
57-
"""
58-
Integer dots per inch for the width of this image. Defaults to 72
59-
when not present in the file, as is often the case.
60-
"""
61-
return self._horz_dpi
62-
63-
@property
64-
def vert_dpi(self):
65-
"""
66-
Integer dots per inch for the height of this image. Defaults to 72
67-
when not present in the file, as is often the case.
68-
"""
69-
return self._vert_dpi
70-
7186

7287
class _JfifMarkers(object):
7388
"""
@@ -78,6 +93,22 @@ def __init__(self, markers):
7893
super(_JfifMarkers, self).__init__()
7994
self._markers = list(markers)
8095

96+
def __str__(self):
97+
"""
98+
Returns a tabular listing of the markers in this instance, which can
99+
be handy for debugging and perhaps other uses.
100+
"""
101+
header = ' offset seglen mc name\n======= ====== == ====='
102+
tmpl = '%7d %6d %02X %s'
103+
rows = []
104+
for marker in self._markers:
105+
rows.append(tmpl % (
106+
marker.offset, marker.segment_length,
107+
ord(marker.marker_code), marker.name
108+
))
109+
lines = [header] + rows
110+
return '\n'.join(lines)
111+
81112
@classmethod
82113
def from_stream(cls, stream):
83114
"""
@@ -102,6 +133,13 @@ def app0(self):
102133
return m
103134
raise KeyError('no APP0 marker in image')
104135

136+
@property
137+
def app1(self):
138+
"""
139+
First APP1 marker in image markers.
140+
"""
141+
raise NotImplementedError
142+
105143
@property
106144
def sof(self):
107145
"""
@@ -267,6 +305,14 @@ def marker_code(self):
267305
"""
268306
return self._marker_code
269307

308+
@property
309+
def name(self):
310+
return JPEG_MARKER_CODE.marker_names[self._marker_code]
311+
312+
@property
313+
def offset(self):
314+
return self._offset
315+
270316
@property
271317
def segment_length(self):
272318
"""

tests/image/test_jpeg.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE
1515
from docx.image.helpers import BIG_ENDIAN, StreamReader
1616
from docx.image.jpeg import (
17-
_App0Marker, Jfif, _JfifMarkers, Jpeg, _Marker, _MarkerFactory,
17+
_App0Marker, Exif, Jfif, _JfifMarkers, Jpeg, _Marker, _MarkerFactory,
1818
_MarkerFinder, _MarkerParser, _SofMarker
1919
)
2020

@@ -24,10 +24,65 @@
2424
class DescribeJpeg(object):
2525

2626
def it_knows_its_content_type(self):
27-
jpeg = Jpeg(None, None, None, None, None)
27+
jpeg = Jpeg(None, None, None, None, None, None)
2828
assert jpeg.content_type == MIME_TYPE.JPEG
2929

3030

31+
class DescribeExif(object):
32+
33+
def it_can_construct_from_an_exif_stream(self, from_stream_fixture):
34+
# fixture ----------------------
35+
(stream_, blob_, filename_, _JfifMarkers_, px_width, px_height,
36+
horz_dpi, vert_dpi) = from_stream_fixture
37+
# exercise ---------------------
38+
exif = Exif.from_stream(stream_, blob_, filename_)
39+
# verify -----------------------
40+
_JfifMarkers_.from_stream.assert_called_once_with(stream_)
41+
assert isinstance(exif, Exif)
42+
assert exif.px_width == px_width
43+
assert exif.px_height == px_height
44+
assert exif.horz_dpi == horz_dpi
45+
assert exif.vert_dpi == vert_dpi
46+
47+
# fixtures -------------------------------------------------------
48+
49+
@pytest.fixture
50+
def blob_(self, request):
51+
return instance_mock(request, bytes)
52+
53+
@pytest.fixture
54+
def filename_(self, request):
55+
return instance_mock(request, str)
56+
57+
@pytest.fixture
58+
def from_stream_fixture(
59+
self, stream_, blob_, filename_, _JfifMarkers_, jfif_markers_):
60+
px_width, px_height = 111, 222
61+
horz_dpi, vert_dpi = 333, 444
62+
jfif_markers_.sof.px_width = px_width
63+
jfif_markers_.sof.px_height = px_height
64+
jfif_markers_.app1.horz_dpi = horz_dpi
65+
jfif_markers_.app1.vert_dpi = vert_dpi
66+
return (
67+
stream_, blob_, filename_, _JfifMarkers_, px_width, px_height,
68+
horz_dpi, vert_dpi
69+
)
70+
71+
@pytest.fixture
72+
def _JfifMarkers_(self, request, jfif_markers_):
73+
_JfifMarkers_ = class_mock(request, 'docx.image.jpeg._JfifMarkers')
74+
_JfifMarkers_.from_stream.return_value = jfif_markers_
75+
return _JfifMarkers_
76+
77+
@pytest.fixture
78+
def jfif_markers_(self, request):
79+
return instance_mock(request, _JfifMarkers)
80+
81+
@pytest.fixture
82+
def stream_(self, request):
83+
return instance_mock(request, BytesIO)
84+
85+
3186
class DescribeJfif(object):
3287

3388
def it_can_construct_from_a_jfif_stream(self, from_stream_fixture):

0 commit comments

Comments
 (0)