Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions .github/workflows/pytest-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,31 @@ jobs:
run: |
python -m pip install -U pip
python -m pip install .
python -m pip uninstall -y pylibjpeg-openjpeg
python -m pip install git+https://github.com/pydicom/pylibjpeg-data
python -m pip install pytest coverage pytest-cov
python -m pip install pytest coverage pytest-cov pydicom

- name: Run pytest
- name: Run pytest with no plugins
run: |
pytest --cov pylibjpeg
pytest --cov=pylibjpeg --cov-append

- name: Install -libjpeg and -openjpeg plugins and rerun pytest
- name: Rerun pytest with -lj plugin
run: |
pip install pydicom

pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
pytest --cov pylibjpeg
pytest --cov=pylibjpeg --cov-append

- name: Rerun pytest with -oj plugin
run: |
pip uninstall -y pylibjpeg-libjpeg
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
pytest --cov=pylibjpeg --cov-append

- name: Rerun pytest with -oj and -lj plugins
run: |
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
pytest --cov=pylibjpeg --cov-append

- name: Send coverage results
if: ${{ success() }}
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![codecov](https://codecov.io/gh/pydicom/pylibjpeg/branch/master/graph/badge.svg)](https://codecov.io/gh/pydicom/pylibjpeg)
[![Build Status](https://travis-ci.org/pydicom/pylibjpeg.svg?branch=master)](https://travis-ci.org/pydicom/pylibjpeg)
[![Build Status](https://github.com/pydicom/pylibjpeg/workflows/build/badge.svg)](https://github.com/pydicom/pylibjpeg/actions?query=workflow%3Abuild)
[![PyPI version](https://badge.fury.io/py/pylibjpeg.svg)](https://badge.fury.io/py/pylibjpeg)
[![Python versions](https://img.shields.io/pypi/pyversions/pylibjpeg.svg)](https://img.shields.io/pypi/pyversions/pylibjpeg.svg)

Expand All @@ -25,13 +25,12 @@ python -m pip install pylibjpeg

### Plugins

By itself *pylibjpeg* is unable to decode any JPEG images, which is where the
plugins come in. To support a given JPEG format or DICOM Transfer Syntax
One or more plugins are required before *pylibjpeg* is able to decode JPEG images. To decode a given JPEG format or DICOM Transfer Syntax
you first have to install the corresponding package:

#### JPEG Format
| Format | Decode? | Encode? | Plugin | Based on |
|---|------|---|---|---|---|
|---|------|---|---|---|
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |
| JPEG 2000 | Yes | No | [pylibjpeg-openjpeg][3] | [openjpeg][4] |

Expand Down
118 changes: 116 additions & 2 deletions pylibjpeg/tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@


HAS_DECODERS = bool(get_decoders())
RUN_JPEG = bool(get_decoders("JPEG"))
RUN_JPEGLS = bool(get_decoders("JPEG-LS"))
RUN_JPEG2K = bool(get_decoders("JPEG 2000"))


@pytest.mark.skipif(HAS_DECODERS, reason="Decoders available")
Expand Down Expand Up @@ -59,8 +62,8 @@ def test_unknown_decoder_type(self):
get_decoders(decoder_type='TEST')


@pytest.mark.skipif(not HAS_DECODERS, reason="Decoders unavailable")
class TestDecoders(object):
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG decoders available")
class TestJPEGDecoders(object):
"""Test decoding."""
def test_decode_str(self):
"""Test passing a str to decode."""
Expand Down Expand Up @@ -109,9 +112,120 @@ def test_specify_decoder(self):
assert isinstance(fpath, str)
arr = decode(fpath, decoder='libjpeg')

@pytest.mark.skipif("openjpeg" in get_decoders(), reason="Have openjpeg")
def test_specify_unknown_decoder(self):
"""Test specifying an unknown decoder."""
fpath = os.path.join(JPEG_DIRECTORY, '10918', 'p1', 'A1.JPG')
assert isinstance(fpath, str)
with pytest.raises(ValueError, match=r"The 'openjpeg' decoder"):
decode(fpath, decoder='openjpeg')


@pytest.mark.skipif(not RUN_JPEGLS, reason="No JPEG-LS decoders available")
class TestJPEGLSDecoders(object):
"""Test decoding JPEG-LS files."""
def setup(self):
self.basedir = os.path.join(JPEG_DIRECTORY, '14495', 'JLS')

def test_decode_str(self):
"""Test passing a str to decode."""
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
assert isinstance(fpath, str)
arr = decode(fpath)

def test_decode_pathlike(self):
"""Test passing a pathlike to decode."""
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
p = Path(fpath)
assert isinstance(p, os.PathLike)
arr = decode(p)

def test_decode_filelike(self):
"""Test passing a filelike to decode."""
bs = BytesIO()

fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
with open(fpath, 'rb') as f:
arr = decode(f)

with open(fpath, 'rb') as f:
bs.write(f.read())

bs.seek(0)
arr = decode(bs)

def test_decode_bytes(self):
"""Test passing bytes to decode."""
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
with open(fpath, 'rb') as f:
data = f.read()

assert isinstance(data, bytes)
arr = decode(data)

def test_specify_decoder(self):
"""Test specifying the decoder."""
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
arr = decode(fpath, decoder='libjpeg')

@pytest.mark.skipif("openjpeg" in get_decoders(), reason="Have openjpeg")
def test_specify_unknown_decoder(self):
"""Test specifying an unknown decoder."""
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
with pytest.raises(ValueError, match=r"The 'openjpeg' decoder"):
decode(fpath, decoder='openjpeg')


@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 decoders available")
class TestJPEG2KDecoders(object):
"""Test decoding JPEG 2000 files."""
def setup(self):
self.basedir = os.path.join(JPEG_DIRECTORY, '15444', '2KLS')

def test_decode_str(self):
"""Test passing a str to decode."""
fpath = os.path.join(self.basedir, '693.j2k')
assert isinstance(fpath, str)
arr = decode(fpath)

def test_decode_pathlike(self):
"""Test passing a pathlike to decode."""
fpath = os.path.join(self.basedir, '693.j2k')
p = Path(fpath)
assert isinstance(p, os.PathLike)
arr = decode(p)

def test_decode_filelike(self):
"""Test passing a filelike to decode."""
bs = BytesIO()

fpath = os.path.join(self.basedir, '693.j2k')
with open(fpath, 'rb') as f:
arr = decode(f)

with open(fpath, 'rb') as f:
bs.write(f.read())

bs.seek(0)
arr = decode(bs)

def test_decode_bytes(self):
"""Test passing bytes to decode."""
fpath = os.path.join(self.basedir, '693.j2k')
with open(fpath, 'rb') as f:
data = f.read()

assert isinstance(data, bytes)
arr = decode(data)

def test_specify_decoder(self):
"""Test specifying the decoder."""
fpath = os.path.join(self.basedir, '693.j2k')
arr = decode(fpath, decoder='openjpeg')

@pytest.mark.skipif("libjpeg" in get_decoders(), reason="Have libjpeg")
def test_specify_unknown_decoder(self):
"""Test specifying an unknown decoder."""
fpath = os.path.join(self.basedir, '693.j2k')
with pytest.raises(ValueError, match=r"The 'libjpeg' decoder"):
decode(fpath, decoder='libjpeg')
45 changes: 25 additions & 20 deletions pylibjpeg/tests/test_pydicom.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
HAS_JPEG_LS_PLUGIN = '1.2.840.10008.1.2.4.80' in decoders
HAS_JPEG_2K_PLUGIN = '1.2.840.10008.1.2.4.90' in decoders

RUN_JPEG = HAS_JPEG_PLUGIN and HAS_PYDICOM
RUN_JPEGLS = HAS_JPEG_LS_PLUGIN and HAS_PYDICOM
RUN_JPEG2K = HAS_JPEG_2K_PLUGIN and HAS_PYDICOM


@pytest.mark.skipif(not HAS_PYDICOM or HAS_PLUGINS, reason="Plugins available")
class TestNoPlugins(object):
Expand Down Expand Up @@ -68,9 +72,9 @@ def test_get_pixeldata_no_lj_syntax(self):
handler.get_pixeldata(ds)


@pytest.mark.skipif(not HAS_PYDICOM or not HAS_PLUGINS, reason="No plugins")
class TestPlugins(object):
"""Test interaction with plugins."""
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
def test_pixel_array(self):
# Should basically just not mess up the usual pydicom behaviour
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
Expand Down Expand Up @@ -106,6 +110,7 @@ def test_should_change_PI(self):
result = handler.should_change_PhotometricInterpretation_to_RGB(None)
assert result is False

@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
def test_missing_required(self):
"""Test missing required element raises."""
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
Expand All @@ -118,6 +123,7 @@ def test_missing_required(self):
with pytest.raises(AttributeError, match=msg):
ds.pixel_array

@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
def test_ybr_full_422(self):
"""Test YBR_FULL_422 data decoded."""
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
Expand All @@ -126,7 +132,7 @@ def test_ybr_full_422(self):
arr = ds.pixel_array


@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_PLUGIN, reason="No plugin")
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
class TestJPEGPlugin(object):
"""Test interaction with plugins that support JPEG."""
uid = '1.2.840.10008.1.2.4.50'
Expand Down Expand Up @@ -154,7 +160,7 @@ def test_pixel_array(self):
assert 255 == arr[95, 50]


@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_LS_PLUGIN, reason="No plugin")
@pytest.mark.skipif(not RUN_JPEGLS, reason="No JPEG-LS plugin")
class TestJPEGLSPlugin(object):
"""Test interaction with plugins that support JPEG-LS."""
uid = '1.2.840.10008.1.2.4.80'
Expand All @@ -175,7 +181,7 @@ def test_pixel_array(self):
)


@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_2K_PLUGIN, reason="No plugin")
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
class TestJPEG2KPlugin(object):
"""Test interaction with plugins that support JPEG 2000."""
uid = '1.2.840.10008.1.2.4.90'
Expand Down Expand Up @@ -213,29 +219,33 @@ def test_pixel_array(self):
[175, 17, 0]] == arr[175:195, 28, :].tolist()


@pytest.mark.skipif(not HAS_PYDICOM or not HAS_PLUGINS, reason="No plugins")
class TestUtils(object):
"""Test the pydicom.utils functions."""
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
def test_generate_frames_single_1s(self):
"""Test with single frame, 1 sample/px."""
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
ds = index['JPEGBaseline_1s_1f_u_08_08.dcm']['ds']
index = get_indexed_datasets('1.2.840.10008.1.2.4.90')
ds = index['693_J2KR.dcm']['ds']
assert 1 == getattr(ds, 'NumberOfFrames', 1)
assert 1 == ds.SamplesPerPixel
frames = generate_frames(ds)
arr = next(frames)
frame_gen = generate_frames(ds)
arr = next(frame_gen)
with pytest.raises(StopIteration):
next(frames)
next(frame_gen)

assert arr.flags.writeable
assert 'uint8' == arr.dtype
assert 'int16' == arr.dtype
assert (ds.Rows, ds.Columns) == arr.shape
assert 64 == arr[76, 22]
assert (
[1022, 1051, 1165, 1442, 1835, 2096, 2074, 1868, 1685, 1603] ==
arr[290, 135:145].tolist()
)

@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
def test_generate_frames_1s(self):
"""Test with multiple frames, 1 sample/px."""
index = get_indexed_datasets('1.2.840.10008.1.2.4.80')
ds = index['emri_small_jpeg_ls_lossless.dcm']['ds']
index = get_indexed_datasets('1.2.840.10008.1.2.4.90')
ds = index['emri_small_jpeg_2k_lossless.dcm']['ds']
assert ds.NumberOfFrames > 1
assert 1 == ds.SamplesPerPixel
frames = generate_frames(ds)
Expand All @@ -246,6 +256,7 @@ def test_generate_frames_1s(self):
assert (ds.Rows, ds.Columns) == arr.shape
assert 163 == arr[12, 23]

@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
def test_generate_frames_3s_0p(self):
"""Test with multiple frames, 3 sample/px, 0 planar conf."""
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
Expand All @@ -260,9 +271,3 @@ def test_generate_frames_3s_0p(self):
assert 'uint8' == arr.dtype
assert (ds.Rows, ds.Columns, 3) == arr.shape
assert [48, 128, 128] == arr[159, 290, :].tolist()

@pytest.mark.skip()
def test_generate_frames_3s_1p(self):
"""Test 3 sample/px, 1 planar conf."""
# No data
pass
33 changes: 31 additions & 2 deletions pylibjpeg/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,29 @@ def decode(data, decoder=None, kwargs=None):
raise ValueError("Unable to decode the data")


def get_decoders(decoder_type='JPEG'):
"""Return a :class:`dict` of JPEG decoders as {package: callable}."""
def get_decoders(decoder_type=None):
"""Return a :class:`dict` of JPEG decoders as {package: callable}.

Parameters
----------
decoder_type : str, optional
The class of decoders to return, one of:

* ``"JPEG"`` - ISO/IEC 10918 JPEG decoders
* ``"JPEG XT"`` - ISO/IEC 18477 JPEG decoders
* ``"JPEG-LS"`` - ISO/IEC 14495 JPEG decoders
* ``"JPEG 2000"`` - ISO/IEC 15444 JPEG decoders
* ``"JPEG XS"`` - ISO/IEC 21122 JPEG decoders
* ``"JPEG XL"`` - ISO/IEC 18181 JPEG decoders

If no `decoder_type` is used then all available decoders will be
returned.

Returns
-------
dict
A dict of ``{'package_name': <decoder function>}``.
"""
entry_points = {
"JPEG" : "pylibjpeg.jpeg_decoders",
"JPEG XT" : "pylibjpeg.jpeg_xt_decoders",
Expand All @@ -96,6 +117,14 @@ def get_decoders(decoder_type='JPEG'):
"JPEG XS" : "pylibjpeg.jpeg_xs_decoders",
"JPEG XL" : "pylibjpeg.jpeg_xl_decoders",
}
if decoder_type is None:
decoders = {}
for entry_point in entry_points.values():
decoders.update({
val.name: val.load() for val in iter_entry_points(entry_point)
})
return decoders

try:
return {
val.name: val.load()
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pylibjpeg-openjpeg