Skip to content

Commit

Permalink
Add tests and remove reconstruct() (pydicom#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
scaramallion authored Jan 7, 2024
1 parent 535e37f commit d743db7
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 636 deletions.
1 change: 1 addition & 0 deletions docs/changes/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Changes
* Switched packaging and build to ``pyproject.toml``
* Added type hints
* Added support for version 2 of the pixel data decoding interface
* Removed ``utils.reconstruct()``
612 changes: 136 additions & 476 deletions libjpeg/_libjpeg.cpp

Large diffs are not rendered by default.

41 changes: 0 additions & 41 deletions libjpeg/_libjpeg.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -133,44 +133,3 @@ def get_parameters(src: bytes) -> Tuple[int, Dict[str, int]]:
}

return status, parameters


cdef extern from "cmd/reconstruct.hpp":
cdef void Reconstruct(
const char *inArray,
const char *outArray,
int colortrafo,
const char *alpha,
bool upsample,
)


def reconstruct(
fin: bytes,
fout: bytes,
colourspace: int,
falpha: Union[bytes, None],
upsample: bool,
) -> None:
"""Decode the JPEG file in `fin` and write it to `fout` as PNM.

Parameters
----------
fin : bytes
The path to the JPEG file to be decoded.
fout : bytes
The path to the decoded PNM file.
colourspace : int
The colourspace transform to apply.
falpha : bytes or None
The path where any decoded alpha channel data will be written,
otherwise ``None`` to not write alpha channel data. Equivalent to the
``-al file`` flag.
upsample : bool
``True`` to disable automatic upsampling, equivalent to the ``-U``
flag.
"""
if falpha is None:
Reconstruct(fin, fout, colourspace, NULL, upsample)
else:
Reconstruct(fin, fout, colourspace, falpha, upsample)
31 changes: 30 additions & 1 deletion libjpeg/tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ def test_decode_bytes():
assert 255 == arr[95, 50]


def test_decode_binaryio():
"""Test decode using binaryio."""
with open(DIR_10918 / "p1" / "A1.JPG", "rb") as f:
arr = decode(f)
assert arr.flags.writeable
assert "uint8" == arr.dtype
assert arr.shape == (257, 255, 4)


def test_decode_raises():
"""Test decode raises if invalid type"""
msg = (
"Invalid type 'NoneType' - must be the path to a JPEG file, a "
"buffer containing the JPEG data or an open JPEG file-like"
)
with pytest.raises(TypeError, match=msg):
decode(None)


def test_v1_invalid_photometric_raises():
"""Test decode_pixel_data raises if invalid photometric interpretation"""
msg = (
r"The \(0028,0004\) Photometric Interpretation element is missing "
"from the dataset"
)
with pytest.raises(ValueError, match=msg):
decode_pixel_data(DIR_10918 / "p1" / "A1.JPG", version=1)


# TODO: convert to using straight JPG data
@pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom")
def test_invalid_colourspace_warns():
Expand Down Expand Up @@ -261,7 +290,7 @@ def test_v2_non_conformant_raises(self):
nr_frames = ds.get("NumberOfFrames", 1)
frame = next(generate_pixel_data_frame(ds.PixelData, nr_frames))
msg = (
r"libjpeg error code '-1038' returned from decode\(\): A "
r"libjpeg error code '-1038' returned from Decode\(\): A "
r"misplaced marker segment was found - scan start must be zero "
r"and scan stop must be 63 for the sequential operating modes"
)
Expand Down
19 changes: 17 additions & 2 deletions libjpeg/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
except ImportError:
HAS_PYDICOM = False

from libjpeg import decode
from libjpeg import decode, decode_pixel_data
from libjpeg.utils import COLOURSPACE
from libjpeg.data import get_indexed_datasets


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


@pytest.fixture
def add_invalid_colour():
COLOURSPACE["INVALID"] = -1
yield
del COLOURSPACE["INVALID"]


@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")
class TestLibrary:
"""Tests for libjpeg itself."""
Expand All @@ -57,7 +65,7 @@ def test_non_conformant_raises(self):
with pytest.raises(RuntimeError, match=msg):
item["ds"].pixel_array

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

with pytest.raises(RuntimeError, match=msg):
decode_pixel_data(
data,
photometric_interpretation="INVALID",
version=2,
)


# ISO/IEC 10918 JPEG
@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")
Expand Down
48 changes: 42 additions & 6 deletions libjpeg/tests/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from libjpeg import get_parameters
from libjpeg.data import get_indexed_datasets, JPEG_DIRECTORY
from libjpeg.utils import LIBJPEG_ERROR_CODES


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


def test_get_parameters_binary():
"""Test get_parameters() using binaryio."""
with open(os.path.join(DIR_10918, "p1", "A1.JPG"), "rb") as f:
params = get_parameters(f)

info = (257, 255, 4, 8)

assert (info[0], info[1]) == (params["rows"], params["columns"])
assert info[2] == params["nr_components"]
assert info[3] == params["precision"]


def test_get_parameters_path():
"""Test get_parameters() using a path."""
params = get_parameters(os.path.join(DIR_10918, "p1", "A1.JPG"))

info = (257, 255, 4, 8)

assert (info[0], info[1]) == (params["rows"], params["columns"])
assert info[2] == params["nr_components"]
assert info[3] == params["precision"]

@pytest.fixture
def remove_error_code():
msg = LIBJPEG_ERROR_CODES[-1038]
del LIBJPEG_ERROR_CODES[-1038]
yield
LIBJPEG_ERROR_CODES[-1038] = msg


@pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies")
def test_get_parameters_unknown_error(remove_error_code):
"""Test get_parameters() using a path."""
msg = (
r"Unknown error code '-1038' returned from GetJPEGParameters\(\): "
r"unexpected EOF while parsing the image"
)
with pytest.raises(RuntimeError, match=msg):
get_parameters(b"\xFF\xD8\xFF\xD8\x01\x02\x03")


@pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom")
def test_non_conformant_raises():
"""Test that a non-conformant JPEG image raises an exception."""
Expand Down Expand Up @@ -166,13 +208,8 @@ def test_jls_lossless(self, fname, info):
# info: (rows, columns, spp, bps)
index = get_indexed_datasets("1.2.840.10008.1.2.4.80")
ds = index[fname]["ds"]

print(fname)

frame = next(self.generate_frames(ds))
params = get_parameters(frame)
print(params)
print(info)

assert (info[0], info[1]) == (params["rows"], params["columns"])
assert info[2] == params["nr_components"]
Expand All @@ -184,7 +221,6 @@ def test_jls_lossy(self, fname, info):
# info: (rows, columns, spp, bps)
index = get_indexed_datasets("1.2.840.10008.1.2.4.81")
ds = index[fname]["ds"]

frame = next(self.generate_frames(ds))
params = get_parameters(frame)

Expand Down
57 changes: 0 additions & 57 deletions libjpeg/tests/test_reconstruct.py

This file was deleted.

59 changes: 6 additions & 53 deletions libjpeg/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ def decode(
required_methods = ["read", "tell", "seek"]
if not all([hasattr(stream, meth) for meth in required_methods]):
raise TypeError(
"The Python object containing the encoded JPEG 2000 data must "
"either be bytes or have read(), tell() and seek() methods."
f"Invalid type '{type(stream).__name__}' - must be the path "
"to a JPEG file, a buffer containing the JPEG data or an open "
"JPEG file-like"
)
buffer = stream.read()

Expand Down Expand Up @@ -222,11 +223,11 @@ def decode_pixel_data(

if code in LIBJPEG_ERROR_CODES:
raise RuntimeError(
f"libjpeg error code '{code}' returned from decode(): "
f"libjpeg error code '{code}' returned from Decode(): "
f"{LIBJPEG_ERROR_CODES[code]} - {msg}"
)

raise RuntimeError(f"Unknown error code '{code}' returned from decode(): {msg}")
raise RuntimeError(f"Unknown error code '{code}' returned from Decode(): {msg}")


def get_parameters(
Expand Down Expand Up @@ -280,53 +281,5 @@ def get_parameters(
)

raise RuntimeError(
f"Unknown error code '{status}' returned from GetJPEGParameters(): {msg}"
f"Unknown error code '{code}' returned from GetJPEGParameters(): {msg}"
)


def reconstruct(
fin: Union[str, os.PathLike, bytes],
fout: Union[str, os.PathLike, bytes],
colourspace: int = 1,
falpha: Union[bytes, None] = None,
upsample: bool = True,
) -> None:
"""Simple wrapper for the libjpeg ``cmd/reconstruct::Reconstruct()``
function.
Parameters
----------
fin : bytes
The path to the JPEG file to be decoded.
fout : bytes
The path to the decoded PPM or PGM (if `falpha` is ``True``) file(s).
colourspace : int, optional
The colourspace transform to apply.
| ``0`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_NONE`` (``-c`` flag)
| ``1`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_YCBCR`` (default)
| ``2`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_LSRCT`` (``-cls`` flag)
| ``2`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_RCT``
| ``3`` : ``JPGFLAG_MATRIX_COLORTRANSFORMATION_FREEFORM``
See `here<https://github.com/thorfdbg/libjpeg/blob/87636f3b26b41b85b2fb7355c589a8c456ef808c/interface/parameters.hpp#L381>`_
for more information.
falpha : bytes, optional
The path where any decoded alpha channel data will be written (as a
PGM file), otherwise ``None`` (default) to not write alpha channel
data. Equivalent to the ``-al file`` flag.
upsample : bool, optional
``True`` (default) to disable automatic upsampling, equivalent to
the ``-U`` flag.
"""
if isinstance(fin, (str, Path)):
fin = str(fin)
fin = bytes(fin, "utf-8")

if isinstance(fout, (str, Path)):
fout = str(fout)
fout = bytes(fout, "utf-8")

if falpha and isinstance(falpha, (str, Path)):
falpha = str(falpha)
falpha = bytes(falpha, "utf-8")

_libjpeg.reconstruct(fin, fout, colourspace, falpha, upsample)

0 comments on commit d743db7

Please sign in to comment.