Skip to content

Commit d743db7

Browse files
authored
Add tests and remove reconstruct() (#61)
1 parent 535e37f commit d743db7

File tree

8 files changed

+232
-636
lines changed

8 files changed

+232
-636
lines changed

docs/changes/v2.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Changes
1111
* Switched packaging and build to ``pyproject.toml``
1212
* Added type hints
1313
* Added support for version 2 of the pixel data decoding interface
14+
* Removed ``utils.reconstruct()``

libjpeg/_libjpeg.cpp

Lines changed: 136 additions & 476 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libjpeg/_libjpeg.pyx

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -133,44 +133,3 @@ def get_parameters(src: bytes) -> Tuple[int, Dict[str, int]]:
133133
}
134134

135135
return status, parameters
136-
137-
138-
cdef extern from "cmd/reconstruct.hpp":
139-
cdef void Reconstruct(
140-
const char *inArray,
141-
const char *outArray,
142-
int colortrafo,
143-
const char *alpha,
144-
bool upsample,
145-
)
146-
147-
148-
def reconstruct(
149-
fin: bytes,
150-
fout: bytes,
151-
colourspace: int,
152-
falpha: Union[bytes, None],
153-
upsample: bool,
154-
) -> None:
155-
"""Decode the JPEG file in `fin` and write it to `fout` as PNM.
156-
157-
Parameters
158-
----------
159-
fin : bytes
160-
The path to the JPEG file to be decoded.
161-
fout : bytes
162-
The path to the decoded PNM file.
163-
colourspace : int
164-
The colourspace transform to apply.
165-
falpha : bytes or None
166-
The path where any decoded alpha channel data will be written,
167-
otherwise ``None`` to not write alpha channel data. Equivalent to the
168-
``-al file`` flag.
169-
upsample : bool
170-
``True`` to disable automatic upsampling, equivalent to the ``-U``
171-
flag.
172-
"""
173-
if falpha is None:
174-
Reconstruct(fin, fout, colourspace, NULL, upsample)
175-
else:
176-
Reconstruct(fin, fout, colourspace, falpha, upsample)

libjpeg/tests/test_decode.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ def test_decode_bytes():
9090
assert 255 == arr[95, 50]
9191

9292

93+
def test_decode_binaryio():
94+
"""Test decode using binaryio."""
95+
with open(DIR_10918 / "p1" / "A1.JPG", "rb") as f:
96+
arr = decode(f)
97+
assert arr.flags.writeable
98+
assert "uint8" == arr.dtype
99+
assert arr.shape == (257, 255, 4)
100+
101+
102+
def test_decode_raises():
103+
"""Test decode raises if invalid type"""
104+
msg = (
105+
"Invalid type 'NoneType' - must be the path to a JPEG file, a "
106+
"buffer containing the JPEG data or an open JPEG file-like"
107+
)
108+
with pytest.raises(TypeError, match=msg):
109+
decode(None)
110+
111+
112+
def test_v1_invalid_photometric_raises():
113+
"""Test decode_pixel_data raises if invalid photometric interpretation"""
114+
msg = (
115+
r"The \(0028,0004\) Photometric Interpretation element is missing "
116+
"from the dataset"
117+
)
118+
with pytest.raises(ValueError, match=msg):
119+
decode_pixel_data(DIR_10918 / "p1" / "A1.JPG", version=1)
120+
121+
93122
# TODO: convert to using straight JPG data
94123
@pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom")
95124
def test_invalid_colourspace_warns():
@@ -261,7 +290,7 @@ def test_v2_non_conformant_raises(self):
261290
nr_frames = ds.get("NumberOfFrames", 1)
262291
frame = next(generate_pixel_data_frame(ds.PixelData, nr_frames))
263292
msg = (
264-
r"libjpeg error code '-1038' returned from decode\(\): A "
293+
r"libjpeg error code '-1038' returned from Decode\(\): A "
265294
r"misplaced marker segment was found - scan start must be zero "
266295
r"and scan stop must be 63 for the sequential operating modes"
267296
)

libjpeg/tests/test_handler.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
except ImportError:
1111
HAS_PYDICOM = False
1212

13-
from libjpeg import decode
13+
from libjpeg import decode, decode_pixel_data
14+
from libjpeg.utils import COLOURSPACE
1415
from libjpeg.data import get_indexed_datasets
1516

1617

@@ -39,6 +40,13 @@ def plot(self, arr, index=None, cmap=None):
3940
plt.show()
4041

4142

43+
@pytest.fixture
44+
def add_invalid_colour():
45+
COLOURSPACE["INVALID"] = -1
46+
yield
47+
del COLOURSPACE["INVALID"]
48+
49+
4250
@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")
4351
class TestLibrary:
4452
"""Tests for libjpeg itself."""
@@ -57,7 +65,7 @@ def test_non_conformant_raises(self):
5765
with pytest.raises(RuntimeError, match=msg):
5866
item["ds"].pixel_array
5967

60-
def test_invalid_colour_transform(self):
68+
def test_invalid_colour_transform(self, add_invalid_colour):
6169
"""Test that an invalid colour transform raises an exception."""
6270
ds_list = get_indexed_datasets("1.2.840.10008.1.2.4.50")
6371
# Image has invalid Se value in the SOS marker segment
@@ -70,6 +78,13 @@ def test_invalid_colour_transform(self):
7078
with pytest.raises(RuntimeError, match=msg):
7179
decode(data, -1)
7280

81+
with pytest.raises(RuntimeError, match=msg):
82+
decode_pixel_data(
83+
data,
84+
photometric_interpretation="INVALID",
85+
version=2,
86+
)
87+
7388

7489
# ISO/IEC 10918 JPEG
7590
@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")

libjpeg/tests/test_parameters.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from libjpeg import get_parameters
1414
from libjpeg.data import get_indexed_datasets, JPEG_DIRECTORY
15+
from libjpeg.utils import LIBJPEG_ERROR_CODES
1516

1617

1718
DIR_10918 = os.path.join(JPEG_DIRECTORY, "10918")
@@ -74,6 +75,47 @@ def test_get_parameters_bytes():
7475
assert info[3] == params["precision"]
7576

7677

78+
def test_get_parameters_binary():
79+
"""Test get_parameters() using binaryio."""
80+
with open(os.path.join(DIR_10918, "p1", "A1.JPG"), "rb") as f:
81+
params = get_parameters(f)
82+
83+
info = (257, 255, 4, 8)
84+
85+
assert (info[0], info[1]) == (params["rows"], params["columns"])
86+
assert info[2] == params["nr_components"]
87+
assert info[3] == params["precision"]
88+
89+
90+
def test_get_parameters_path():
91+
"""Test get_parameters() using a path."""
92+
params = get_parameters(os.path.join(DIR_10918, "p1", "A1.JPG"))
93+
94+
info = (257, 255, 4, 8)
95+
96+
assert (info[0], info[1]) == (params["rows"], params["columns"])
97+
assert info[2] == params["nr_components"]
98+
assert info[3] == params["precision"]
99+
100+
@pytest.fixture
101+
def remove_error_code():
102+
msg = LIBJPEG_ERROR_CODES[-1038]
103+
del LIBJPEG_ERROR_CODES[-1038]
104+
yield
105+
LIBJPEG_ERROR_CODES[-1038] = msg
106+
107+
108+
@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")
109+
def test_get_parameters_unknown_error(remove_error_code):
110+
"""Test get_parameters() using a path."""
111+
msg = (
112+
r"Unknown error code '-1038' returned from GetJPEGParameters\(\): "
113+
r"unexpected EOF while parsing the image"
114+
)
115+
with pytest.raises(RuntimeError, match=msg):
116+
get_parameters(b"\xFF\xD8\xFF\xD8\x01\x02\x03")
117+
118+
77119
@pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom")
78120
def test_non_conformant_raises():
79121
"""Test that a non-conformant JPEG image raises an exception."""
@@ -166,13 +208,8 @@ def test_jls_lossless(self, fname, info):
166208
# info: (rows, columns, spp, bps)
167209
index = get_indexed_datasets("1.2.840.10008.1.2.4.80")
168210
ds = index[fname]["ds"]
169-
170-
print(fname)
171-
172211
frame = next(self.generate_frames(ds))
173212
params = get_parameters(frame)
174-
print(params)
175-
print(info)
176213

177214
assert (info[0], info[1]) == (params["rows"], params["columns"])
178215
assert info[2] == params["nr_components"]
@@ -184,7 +221,6 @@ def test_jls_lossy(self, fname, info):
184221
# info: (rows, columns, spp, bps)
185222
index = get_indexed_datasets("1.2.840.10008.1.2.4.81")
186223
ds = index[fname]["ds"]
187-
188224
frame = next(self.generate_frames(ds))
189225
params = get_parameters(frame)
190226

libjpeg/tests/test_reconstruct.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

libjpeg/utils.py

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ def decode(
118118
required_methods = ["read", "tell", "seek"]
119119
if not all([hasattr(stream, meth) for meth in required_methods]):
120120
raise TypeError(
121-
"The Python object containing the encoded JPEG 2000 data must "
122-
"either be bytes or have read(), tell() and seek() methods."
121+
f"Invalid type '{type(stream).__name__}' - must be the path "
122+
"to a JPEG file, a buffer containing the JPEG data or an open "
123+
"JPEG file-like"
123124
)
124125
buffer = stream.read()
125126

@@ -222,11 +223,11 @@ def decode_pixel_data(
222223

223224
if code in LIBJPEG_ERROR_CODES:
224225
raise RuntimeError(
225-
f"libjpeg error code '{code}' returned from decode(): "
226+
f"libjpeg error code '{code}' returned from Decode(): "
226227
f"{LIBJPEG_ERROR_CODES[code]} - {msg}"
227228
)
228229

229-
raise RuntimeError(f"Unknown error code '{code}' returned from decode(): {msg}")
230+
raise RuntimeError(f"Unknown error code '{code}' returned from Decode(): {msg}")
230231

231232

232233
def get_parameters(
@@ -280,53 +281,5 @@ def get_parameters(
280281
)
281282

282283
raise RuntimeError(
283-
f"Unknown error code '{status}' returned from GetJPEGParameters(): {msg}"
284+
f"Unknown error code '{code}' returned from GetJPEGParameters(): {msg}"
284285
)
285-
286-
287-
def reconstruct(
288-
fin: Union[str, os.PathLike, bytes],
289-
fout: Union[str, os.PathLike, bytes],
290-
colourspace: int = 1,
291-
falpha: Union[bytes, None] = None,
292-
upsample: bool = True,
293-
) -> None:
294-
"""Simple wrapper for the libjpeg ``cmd/reconstruct::Reconstruct()``
295-
function.
296-
297-
Parameters
298-
----------
299-
fin : bytes
300-
The path to the JPEG file to be decoded.
301-
fout : bytes
302-
The path to the decoded PPM or PGM (if `falpha` is ``True``) file(s).
303-
colourspace : int, optional
304-
The colourspace transform to apply.
305-
| ``0`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_NONE`` (``-c`` flag)
306-
| ``1`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_YCBCR`` (default)
307-
| ``2`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_LSRCT`` (``-cls`` flag)
308-
| ``2`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_RCT``
309-
| ``3`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_FREEFORM``
310-
See `here<https://github.com/thorfdbg/libjpeg/blob/87636f3b26b41b85b2fb7355c589a8c456ef808c/interface/parameters.hpp#L381>`_
311-
for more information.
312-
falpha : bytes, optional
313-
The path where any decoded alpha channel data will be written (as a
314-
PGM file), otherwise ``None`` (default) to not write alpha channel
315-
data. Equivalent to the ``-al file`` flag.
316-
upsample : bool, optional
317-
``True`` (default) to disable automatic upsampling, equivalent to
318-
the ``-U`` flag.
319-
"""
320-
if isinstance(fin, (str, Path)):
321-
fin = str(fin)
322-
fin = bytes(fin, "utf-8")
323-
324-
if isinstance(fout, (str, Path)):
325-
fout = str(fout)
326-
fout = bytes(fout, "utf-8")
327-
328-
if falpha and isinstance(falpha, (str, Path)):
329-
falpha = str(falpha)
330-
falpha = bytes(falpha, "utf-8")
331-
332-
_libjpeg.reconstruct(fin, fout, colourspace, falpha, upsample)

0 commit comments

Comments
 (0)