From 8cf4077713725ee069f2f197d9b2e24c47687eb6 Mon Sep 17 00:00:00 2001 From: scaramallion Date: Sat, 13 Jun 2020 10:03:03 +1000 Subject: [PATCH 1/4] Change report uploads --- .github/workflows/pytest-builds.yml | 15 ++++++--------- README.md | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index b4d6543..1791421 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -32,19 +32,16 @@ jobs: - name: Run pytest run: | - pytest --cov pylibjpeg + pytest --cov=pylibjpeg --cov-append - name: Install -libjpeg and -openjpeg plugins and rerun pytest 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: Send coverage results - if: ${{ success() }} - run: | - bash <(curl --connect-timeout 10 --retry 10 --retry-max-time \ - 0 https://codecov.io/bash) || (sleep 30 && bash <(curl \ - --connect-timeout 10 --retry 10 --retry-max-time \ - 0 https://codecov.io/bash)) + - uses: codecov/codecov-action@v1 + name: Upload coverage results + with: + file: .coverage diff --git a/README.md b/README.md index 1ef985a..2358514 100644 --- a/README.md +++ b/README.md @@ -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) From 410236e4416cc0333e6406c7ade8aa9a0d132bf2 Mon Sep 17 00:00:00 2001 From: scaramallion Date: Sat, 13 Jun 2020 10:35:55 +1000 Subject: [PATCH 2/4] Fix up test skips --- .github/workflows/pytest-builds.yml | 12 +++++--- README.md | 5 ++-- pylibjpeg/tests/test_pydicom.py | 45 ++++++++++++++++------------- requirements.txt | 1 + 4 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 requirements.txt diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index 1791421..eac5c81 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -27,6 +27,7 @@ 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 @@ -41,7 +42,10 @@ jobs: pip install git+https://github.com/pydicom/pylibjpeg-openjpeg pytest --cov=pylibjpeg --cov-append - - uses: codecov/codecov-action@v1 - name: Upload coverage results - with: - file: .coverage + - name: Send coverage results + if: ${{ success() }} + run: | + bash <(curl --connect-timeout 10 --retry 10 --retry-max-time \ + 0 https://codecov.io/bash) || (sleep 30 && bash <(curl \ + --connect-timeout 10 --retry 10 --retry-max-time \ + 0 https://codecov.io/bash)) diff --git a/README.md b/README.md index 2358514..8ccd8ff 100644 --- a/README.md +++ b/README.md @@ -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] | diff --git a/pylibjpeg/tests/test_pydicom.py b/pylibjpeg/tests/test_pydicom.py index 9899d5e..1057c83 100644 --- a/pylibjpeg/tests/test_pydicom.py +++ b/pylibjpeg/tests/test_pydicom.py @@ -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): @@ -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') @@ -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') @@ -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') @@ -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' @@ -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' @@ -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' @@ -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) @@ -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') @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6a96010 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pylibjpeg-openjpeg From 3a57056a7ed8e36eb8452a21dd9198ae689bc98a Mon Sep 17 00:00:00 2001 From: scaramallion Date: Sat, 13 Jun 2020 11:04:44 +1000 Subject: [PATCH 3/4] Add tests, change CI builds --- .github/workflows/pytest-builds.yml | 15 +++- pylibjpeg/tests/test_decode.py | 118 +++++++++++++++++++++++++++- pylibjpeg/utils.py | 33 +++++++- 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index eac5c81..9e69850 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -31,17 +31,28 @@ jobs: python -m pip install git+https://github.com/pydicom/pylibjpeg-data python -m pip install pytest coverage pytest-cov - - name: Run pytest + - name: Run pytest with no plugins run: | 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 --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() }} run: | diff --git a/pylibjpeg/tests/test_decode.py b/pylibjpeg/tests/test_decode.py index 67f01d3..58b6c03 100644 --- a/pylibjpeg/tests/test_decode.py +++ b/pylibjpeg/tests/test_decode.py @@ -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") @@ -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.""" @@ -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') diff --git a/pylibjpeg/utils.py b/pylibjpeg/utils.py index b41e16f..bb52205 100644 --- a/pylibjpeg/utils.py +++ b/pylibjpeg/utils.py @@ -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': }``. + """ entry_points = { "JPEG" : "pylibjpeg.jpeg_decoders", "JPEG XT" : "pylibjpeg.jpeg_xt_decoders", @@ -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() From c2a2724e5c3b7be65e0d9821308f8eee6a626a2b Mon Sep 17 00:00:00 2001 From: scaramallion Date: Sat, 13 Jun 2020 11:06:18 +1000 Subject: [PATCH 4/4] Fix missing dep --- .github/workflows/pytest-builds.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index 9e69850..82d359e 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -29,7 +29,7 @@ jobs: 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 with no plugins run: | @@ -37,7 +37,7 @@ jobs: - 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 --cov-append