Skip to content

Commit 96774c5

Browse files
author
Steve Canny
committed
img: add _JfifMarkers.from_stream()
1 parent 1e6d9ba commit 96774c5

3 files changed

Lines changed: 230 additions & 5 deletions

File tree

docx/image/constants.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,94 @@
55
"""
66

77

8+
class JPEG_MARKER_CODE(object):
9+
"""
10+
JPEG marker codes
11+
"""
12+
TEM = b'\x01'
13+
DHT = b'\xC4'
14+
DAC = b'\xCC'
15+
JPG = b'\xC8'
16+
17+
SOF0 = b'\xC0'
18+
SOF1 = b'\xC1'
19+
SOF2 = b'\xC2'
20+
SOF3 = b'\xC3'
21+
SOF5 = b'\xC5'
22+
SOF6 = b'\xC6'
23+
SOF7 = b'\xC7'
24+
SOF9 = b'\xC9'
25+
SOFA = b'\xCA'
26+
SOFB = b'\xCB'
27+
SOFD = b'\xCD'
28+
SOFE = b'\xCE'
29+
SOFF = b'\xCF'
30+
31+
RST0 = b'\xD0'
32+
RST1 = b'\xD1'
33+
RST2 = b'\xD2'
34+
RST3 = b'\xD3'
35+
RST4 = b'\xD4'
36+
RST5 = b'\xD5'
37+
RST6 = b'\xD6'
38+
RST7 = b'\xD7'
39+
40+
SOI = b'\xD8'
41+
EOI = b'\xD9'
42+
SOS = b'\xDA'
43+
DQT = b'\xDB' # Define Quantization Table(s)
44+
DNL = b'\xDC'
45+
DRI = b'\xDD'
46+
DHP = b'\xDE'
47+
EXP = b'\xDF'
48+
49+
APP0 = b'\xE0'
50+
APP1 = b'\xE1'
51+
APP2 = b'\xE2'
52+
APP3 = b'\xE3'
53+
APP4 = b'\xE4'
54+
APP5 = b'\xE5'
55+
APP6 = b'\xE6'
56+
APP7 = b'\xE7'
57+
APP8 = b'\xE8'
58+
APP9 = b'\xE9'
59+
APPA = b'\xEA'
60+
APPB = b'\xEB'
61+
APPC = b'\xEC'
62+
APPD = b'\xED'
63+
APPE = b'\xEE'
64+
APPF = b'\xEF'
65+
66+
STANDALONE_MARKERS = (
67+
TEM, SOI, EOI, RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7
68+
)
69+
70+
SOF_MARKER_CODES = (
71+
SOF0, SOF1, SOF2, SOF3, SOF5, SOF6, SOF7, SOF9, SOFA, SOFB, SOFD,
72+
SOFE, SOFF
73+
)
74+
75+
names = {
76+
b'\x00': 'UNKNOWN',
77+
b'\xC0': 'SOF0',
78+
b'\xC2': 'SOF2',
79+
b'\xC4': 'DHT',
80+
b'\xDA': 'SOS', # start of scan
81+
b'\xD8': 'SOI', # start of image
82+
b'\xD9': 'EOI', # end of image
83+
b'\xDB': 'DQT',
84+
b'\xE0': 'APP0',
85+
b'\xE1': 'APP1',
86+
b'\xE2': 'APP2',
87+
b'\xED': 'APP13',
88+
b'\xEE': 'APP14',
89+
}
90+
91+
@classmethod
92+
def is_standalone(cls, marker_code):
93+
return marker_code in cls.STANDALONE_MARKERS
94+
95+
896
class MIME_TYPE(object):
997
"""
1098
Image content types.

docx/image/jpeg.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import absolute_import, division, print_function
99

10+
from .constants import JPEG_MARKER_CODE
1011
from .image import Image
1112

1213

@@ -65,13 +66,23 @@ class _JfifMarkers(object):
6566
Sequence of markers in a JPEG file, perhaps truncated at first SOS marker
6667
for performance reasons.
6768
"""
69+
def __init__(self, markers):
70+
super(_JfifMarkers, self).__init__()
71+
self._markers = list(markers)
72+
6873
@classmethod
6974
def from_stream(cls, stream):
7075
"""
71-
Return a |_JfifMarkers| instance containing a |_JfifMarker| instance
72-
for each marker in *stream*.
76+
Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass
77+
instance for each marker in *stream*.
7378
"""
74-
raise NotImplementedError
79+
marker_parser = _MarkerParser.from_stream(stream)
80+
markers = []
81+
for marker in marker_parser.iter_markers():
82+
markers.append(marker)
83+
if marker.marker_code == JPEG_MARKER_CODE.SOS:
84+
break
85+
return cls(markers)
7586

7687
@property
7788
def app0(self):
@@ -86,3 +97,51 @@ def sof(self):
8697
First start of frame (SOFn) marker in this sequence.
8798
"""
8899
raise NotImplementedError
100+
101+
102+
class _MarkerParser(object):
103+
"""
104+
Service class that knows how to parse a JFIF stream and iterate over its
105+
markers.
106+
"""
107+
@classmethod
108+
def from_stream(cls, stream):
109+
"""
110+
Return a |_MarkerParser| instance to parse JFIF markers from
111+
*stream*.
112+
"""
113+
raise NotImplementedError
114+
115+
def iter_markers(self):
116+
"""
117+
Generate a (marker_code, segment_offset) 3-tuple for each marker in
118+
the JPEG byte stream in *stream*, in the order they occur in the
119+
stream.
120+
"""
121+
raise NotImplementedError
122+
123+
124+
class _Marker(object):
125+
"""
126+
Base class for JFIF marker classes. Represents a marker and its segment
127+
occuring in a JPEG byte stream.
128+
"""
129+
@property
130+
def marker_code(self):
131+
"""
132+
The single-byte code that identifies the type of this marker, e.g.
133+
``'\xE0'`` for start of image (SOI).
134+
"""
135+
raise NotImplementedError
136+
137+
138+
class _App0Marker(_Marker):
139+
"""
140+
Represents a JFIF APP0 marker segment.
141+
"""
142+
143+
144+
class _SofMarker(_Marker):
145+
"""
146+
Represents a JFIF start of frame (SOFx) marker segment.
147+
"""

tests/image/test_jpeg.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
import pytest
1010

1111
from docx.compat import BytesIO
12-
from docx.image.jpeg import Jfif, _JfifMarkers
12+
from docx.image.constants import JPEG_MARKER_CODE
13+
from docx.image.jpeg import (
14+
_App0Marker, Jfif, _JfifMarkers, _Marker, _MarkerParser, _SofMarker
15+
)
1316

14-
from ..unitutil import class_mock, instance_mock
17+
from ..unitutil import class_mock, initializer_mock, instance_mock
1518

1619

1720
class DescribeJfif(object):
@@ -67,3 +70,78 @@ def jfif_markers_(self, request):
6770
@pytest.fixture
6871
def stream_(self, request):
6972
return instance_mock(request, BytesIO)
73+
74+
75+
class Describe_JfifMarkers(object):
76+
77+
def it_can_construct_from_a_jfif_stream(self, from_stream_fixture):
78+
stream_, _MarkerParser_, _JfifMarkers__init_, marker_lst = (
79+
from_stream_fixture
80+
)
81+
jfif_markers = _JfifMarkers.from_stream(stream_)
82+
_MarkerParser_.from_stream.assert_called_once_with(stream_)
83+
_JfifMarkers__init_.assert_called_once_with(marker_lst)
84+
assert isinstance(jfif_markers, _JfifMarkers)
85+
86+
# fixtures -------------------------------------------------------
87+
88+
@pytest.fixture
89+
def app0_(self, request):
90+
return instance_mock(
91+
request, _App0Marker, marker_code=JPEG_MARKER_CODE.APP0
92+
)
93+
94+
@pytest.fixture
95+
def eoi_(self, request):
96+
return instance_mock(
97+
request, _SofMarker, marker_code=JPEG_MARKER_CODE.EOI
98+
)
99+
100+
@pytest.fixture
101+
def from_stream_fixture(
102+
self, stream_, _MarkerParser_, _JfifMarkers__init_, soi_, app0_,
103+
sof_, sos_):
104+
marker_lst = [soi_, app0_, sof_, sos_]
105+
return stream_, _MarkerParser_, _JfifMarkers__init_, marker_lst
106+
107+
@pytest.fixture
108+
def _JfifMarkers__init_(self, request):
109+
return initializer_mock(request, _JfifMarkers)
110+
111+
@pytest.fixture
112+
def marker_parser_(self, request, markers_all_):
113+
marker_parser_ = instance_mock(request, _MarkerParser)
114+
marker_parser_.iter_markers.return_value = markers_all_
115+
return marker_parser_
116+
117+
@pytest.fixture
118+
def _MarkerParser_(self, request, marker_parser_):
119+
_MarkerParser_ = class_mock(request, 'docx.image.jpeg._MarkerParser')
120+
_MarkerParser_.from_stream.return_value = marker_parser_
121+
return _MarkerParser_
122+
123+
@pytest.fixture
124+
def markers_all_(self, request, soi_, app0_, sof_, sos_, eoi_):
125+
return [soi_, app0_, sof_, sos_, eoi_]
126+
127+
@pytest.fixture
128+
def sof_(self, request):
129+
return instance_mock(
130+
request, _SofMarker, marker_code=JPEG_MARKER_CODE.SOF0
131+
)
132+
133+
@pytest.fixture
134+
def soi_(self, request):
135+
return instance_mock(
136+
request, _Marker, marker_code=JPEG_MARKER_CODE.SOI
137+
)
138+
139+
@pytest.fixture
140+
def sos_(self, request):
141+
return instance_mock(
142+
request, _Marker, marker_code=JPEG_MARKER_CODE.SOS
143+
)
144+
145+
@pytest.fixture
146+
def stream_(self, request):
147+
return instance_mock(request, BytesIO)

0 commit comments

Comments
 (0)