Skip to content

Commit 578f476

Browse files
author
Steve Canny
committed
img: add Png._parse_chunks()
1 parent a2782f5 commit 578f476

4 files changed

Lines changed: 114 additions & 22 deletions

File tree

docx/image/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# encoding: utf-8
2+
3+
"""
4+
Constants specific the the image sub-package
5+
"""
6+
7+
8+
class TAG(object):
9+
10+
PX_WIDTH = 'px_width'
11+
PX_HEIGHT = 'px_height'
12+
HORZ_PX_PER_UNIT = 'horz_px_per_unit'
13+
VERT_PX_PER_UNIT = 'vert_px_per_unit'
14+
UNITS_SPECIFIER = 'units_specifier'

docx/image/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# encoding: utf-8
22

3+
"""
4+
Exceptions specific the the image sub-package
5+
"""
6+
37

48
class InvalidImageStreamError(Exception):
59
"""

docx/image/png.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from .image import Image
88

99

10+
_CHUNK_TYPE_IHDR = 'IHDR'
11+
_CHUNK_TYPE_pHYs = 'pHYs'
12+
_CHUNK_TYPE_IEND = 'IEND'
13+
14+
1015
class Png(Image):
1116
"""
1217
Image header parser for PNG images
@@ -29,9 +34,6 @@ def _parse_png_headers(cls, stream):
2934
*stream*.
3035
"""
3136
chunk_offsets = cls._parse_chunk_offsets(stream)
32-
# IHDR chunk is mandatory, invalid if not present
33-
if 'IHDR' not in chunk_offsets:
34-
raise InvalidImageStreamError('no IHDR chunk in PNG image')
3537
attrs = cls._parse_chunks(stream, chunk_offsets)
3638
return attrs
3739

@@ -62,15 +64,47 @@ def _iter_chunk_offsets(stream):
6264
chunk_type = stream.read_str(4, chunk_offset, 4)
6365
data_offset = chunk_offset + 8
6466
yield chunk_type, data_offset
65-
if chunk_type == 'IEND':
67+
if chunk_type == _CHUNK_TYPE_IEND:
6668
break
6769
# incr offset for chunk len long, chunk type, chunk data, and CRC
6870
chunk_offset += (4 + 4 + chunk_data_len + 4)
6971

7072
@classmethod
7173
def _parse_chunks(cls, stream, chunk_offsets):
7274
"""
73-
Return a dict of field, value pairs parsed from the chunks in
74-
*stream* having offsets in *chunk_offsets*.
75+
Return a dict of field, value pairs parsed from selected chunks in
76+
the PNG image in *stream*, using *chunk_offsets* to locate the
77+
desired chunks.
78+
"""
79+
attrs = {}
80+
81+
if _CHUNK_TYPE_IHDR not in chunk_offsets:
82+
# IHDR chunk is mandatory, invalid if not present
83+
raise InvalidImageStreamError('no IHDR chunk in PNG image')
84+
ihdr_offset = chunk_offsets[_CHUNK_TYPE_IHDR]
85+
ihdr_attrs = cls._parse_IHDR(stream, ihdr_offset)
86+
attrs.update(ihdr_attrs)
87+
88+
if _CHUNK_TYPE_pHYs in chunk_offsets:
89+
phys_offset = chunk_offsets[_CHUNK_TYPE_pHYs]
90+
phys_attrs = cls._parse_pHYs(stream, phys_offset)
91+
attrs.update(phys_attrs)
92+
93+
return attrs
94+
95+
@classmethod
96+
def _parse_IHDR(cls, stream, offset):
97+
"""
98+
Return a dict containing values for TAG.PX_WIDTH and TAG.PX_HEIGHT
99+
extracted from the IHDR chunk in *stream* at *offset*.
100+
"""
101+
raise NotImplementedError
102+
103+
@classmethod
104+
def _parse_pHYs(cls, stream, offset):
105+
"""
106+
Return a dict containing values for TAG.HORZ_PX_PER_UNIT,
107+
TAG.VERT_PX_PER_UNIT, and TAG.UNITS_SPECIFIER parsed from the pHYs
108+
chunk at *offset* in *stream*.
75109
"""
76110
raise NotImplementedError

tests/image/test_png.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010

1111
from docx.compat import BytesIO
12+
from docx.image.constants import TAG
1213
from docx.image.exceptions import InvalidImageStreamError
1314
from docx.image.helpers import BIG_ENDIAN, StreamReader
1415
from docx.image.png import Png
@@ -33,24 +34,33 @@ def it_can_construct_from_a_png_stream(self, from_stream_fixture):
3334
assert isinstance(png, Png)
3435

3536
def it_parses_PNG_headers_to_access_attrs(self, parse_png_fixture):
36-
(stream_, _parse_chunk_offsets_, _parse_chunks_, chunk_offsets_,
37+
(stream_, _parse_chunk_offsets_, _parse_chunks_, chunk_offsets,
3738
attrs_) = parse_png_fixture
3839
attrs = Png._parse_png_headers(stream_)
3940
_parse_chunk_offsets_.assert_called_once_with(stream_)
40-
_parse_chunks_.assert_called_once_with(stream_, chunk_offsets_)
41+
_parse_chunks_.assert_called_once_with(stream_, chunk_offsets)
4142
assert attrs == attrs_
4243

43-
def it_raises_on_png_having_no_IHDR_chunk(self, no_IHDR_fixture):
44-
stream_ = no_IHDR_fixture
45-
with pytest.raises(InvalidImageStreamError):
46-
Png._parse_png_headers(stream_)
47-
4844
def it_parses_chunk_offsets_to_help_chunk_parser(
4945
self, chunk_offset_fixture):
5046
stream, expected_chunk_offsets = chunk_offset_fixture
5147
chunk_offsets = Png._parse_chunk_offsets(stream)
5248
assert chunk_offsets == expected_chunk_offsets
5349

50+
def it_parses_chunks_to_extract_fields(self, parse_chunks_fixture):
51+
(stream_, chunk_offsets, _parse_IHDR_, ihdr_offset, _parse_pHYs_,
52+
phys_offset, expected_attrs) = parse_chunks_fixture
53+
attrs = Png._parse_chunks(stream_, chunk_offsets)
54+
_parse_IHDR_.assert_called_once_with(stream_, ihdr_offset)
55+
if phys_offset is not None:
56+
_parse_pHYs_.assert_called_once_with(stream_, phys_offset)
57+
assert attrs == expected_attrs
58+
59+
def it_raises_on_png_having_no_IHDR_chunk(self, no_IHDR_fixture):
60+
stream_, chunk_offsets = no_IHDR_fixture
61+
with pytest.raises(InvalidImageStreamError):
62+
Png._parse_chunks(stream_, chunk_offsets)
63+
5464
# fixtures -------------------------------------------------------
5565

5666
@pytest.fixture
@@ -82,7 +92,7 @@ def chunk_offset_fixture(self, request):
8292
return stream_rdr, expected_chunk_offsets
8393

8494
@pytest.fixture
85-
def chunk_offsets_(self, request):
95+
def chunk_offsets(self, request):
8696
return dict()
8797

8898
@pytest.fixture
@@ -101,24 +111,37 @@ def from_stream_fixture(
101111
)
102112

103113
@pytest.fixture
104-
def no_IHDR_fixture(
105-
self, stream_, _parse_chunk_offsets_, _parse_chunks_):
106-
return stream_
114+
def no_IHDR_fixture(self, stream_, chunk_offsets):
115+
return stream_, chunk_offsets
116+
117+
@pytest.fixture(params=[(42, 24), (42, None)])
118+
def parse_chunks_fixture(
119+
self, request, stream_rdr_, _parse_IHDR_, _parse_pHYs_):
120+
ihdr_offset, phys_offset = request.param
121+
chunk_offsets = {'IHDR': ihdr_offset}
122+
expected_attrs = dict(_parse_IHDR_.return_value)
123+
if phys_offset is not None:
124+
chunk_offsets['pHYs'] = phys_offset
125+
expected_attrs.update(_parse_pHYs_.return_value)
126+
return (
127+
stream_rdr_, chunk_offsets, _parse_IHDR_, ihdr_offset,
128+
_parse_pHYs_, phys_offset, expected_attrs
129+
)
107130

108131
@pytest.fixture
109132
def parse_png_fixture(
110133
self, stream_rdr_, _parse_chunk_offsets_, _parse_chunks_,
111-
chunk_offsets_, attrs_):
112-
chunk_offsets_['IHDR'] = 666
134+
chunk_offsets, attrs_):
135+
chunk_offsets['IHDR'] = 666
113136
return (
114137
stream_rdr_, _parse_chunk_offsets_, _parse_chunks_,
115-
chunk_offsets_, attrs_
138+
chunk_offsets, attrs_
116139
)
117140

118141
@pytest.fixture
119-
def _parse_chunk_offsets_(self, request, chunk_offsets_):
142+
def _parse_chunk_offsets_(self, request, chunk_offsets):
120143
return method_mock(
121-
request, Png, '_parse_chunk_offsets', return_value=chunk_offsets_
144+
request, Png, '_parse_chunk_offsets', return_value=chunk_offsets
122145
)
123146

124147
@pytest.fixture
@@ -127,6 +150,23 @@ def _parse_chunks_(self, request, attrs_):
127150
request, Png, '_parse_chunks', return_value=attrs_
128151
)
129152

153+
@pytest.fixture
154+
def _parse_IHDR_(self, request):
155+
return method_mock(
156+
request, Png, '_parse_IHDR', return_value={
157+
TAG.PX_WIDTH: 12, TAG.PX_HEIGHT: 34
158+
}
159+
)
160+
161+
@pytest.fixture
162+
def _parse_pHYs_(self, request):
163+
return method_mock(
164+
request, Png, '_parse_pHYs', return_value={
165+
TAG.HORZ_PX_PER_UNIT: 56, TAG.VERT_PX_PER_UNIT: 78,
166+
TAG.UNITS_SPECIFIER: 1
167+
}
168+
)
169+
130170
@pytest.fixture
131171
def _parse_png_headers_(self, request, attrs):
132172
return method_mock(

0 commit comments

Comments
 (0)