From 1719d2f11d298d689cce0aedcbc8cdbbf337f110 Mon Sep 17 00:00:00 2001 From: Isotr0py Date: Thu, 10 Oct 2024 23:30:00 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A7(CI):=20Fix=20broken=20release?= =?UTF-8?q?=20and=20test=20CI=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/benchmarks.yml | 17 +++++++++++++---- .github/workflows/release.yml | 17 ++++++++--------- .github/workflows/test.yml | 17 +++++++++++++---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e0c3799..6e789fe 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -1,6 +1,14 @@ name: Benchmarks -on: [push, pull_request] +on: + # Trigger the workflow on push or pull request, + # but only for the main branch + push: + branches: + - main + pull_request: + branches: + - main env: RUSTFLAGS: -C debuginfo=0 # Do not produce debug symbols to keep memory usage down @@ -41,9 +49,10 @@ jobs: - name: Install Plugin run: | brew install jpeg-xl - export DEP_JXL_LIB=/opt/homebrew/Cellar/jpeg-xl/0.11.0/lib - export DEP_BROTLI_LIB=/opt/homebrew/Cellar/brotli/1.1.0/lib - export DEP_HWY_LIB=/opt/homebrew/Cellar/highway/1.2.0/lib + export DEP_JXL_LIB=$(brew --prefix jpeg-xl)'/lib' + export DEP_BROTLI_LIB=$(brew --prefix brotli)'/lib' + export DEP_HWY_LIB=$(brew --prefix highway)'/lib' + source venv/bin/activate pip install maturin maturin develop --features dynamic diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e6c600..3eb12f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '>=3.9 <=3.13' architecture: ${{ matrix.target }} - name: Build wheels @@ -135,24 +135,23 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '>=3.9 <=3.13' - name: Check dependencys run: | brew install jpeg-xl - ls /opt/homebrew/Cellar/jpeg-xl/ - ls /opt/homebrew/Cellar/brotli/ - ls /opt/homebrew/Cellar/highway/ + echo DEP_JXL_LIB=$(brew --prefix jpeg-xl)'/lib' >> $GITHUB_ENV + echo DEP_BROTLI_LIB=$(brew --prefix brotli)'/lib' >> $GITHUB_ENV + echo DEP_HWY_LIB=$(brew --prefix highway)'/lib' >> $GITHUB_ENV - name: Build wheels uses: PyO3/maturin-action@v1 env: RUST_BACKTRACE: 1 MACOSX_DEPLOYMENT_TARGET: 12.7 - # from homebrew - DEP_JXL_LIB: /opt/homebrew/Cellar/jpeg-xl/0.11.0/lib - DEP_BROTLI_LIB: /opt/homebrew/Cellar/brotli/1.1.0/lib - DEP_HWY_LIB: /opt/homebrew/Cellar/highway/1.2.0/lib + DEP_JXL_LIB: ${{ env.DEP_JXL_LIB }} + DEP_BROTLI_LIB: ${{ env.DEP_BROTLI_LIB }} + DEP_HWY_LIB: ${{ env.DEP_HWY_LIB }} with: target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter --features dynamic diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6630949..e411947 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,14 @@ name: Test Python -on: [push, pull_request] +on: + # Trigger the workflow on push or pull request, + # but only for the main branch + push: + branches: + - main + pull_request: + branches: + - main env: RUSTFLAGS: -C debuginfo=0 # Do not produce debug symbols to keep memory usage down @@ -41,9 +49,10 @@ jobs: - name: Install Plugin run: | brew install jpeg-xl - export DEP_JXL_LIB=/opt/homebrew/Cellar/jpeg-xl/0.11.0/lib - export DEP_BROTLI_LIB=/opt/homebrew/Cellar/brotli/1.1.0/lib - export DEP_HWY_LIB=/opt/homebrew/Cellar/highway/1.2.0/lib + export DEP_JXL_LIB=$(brew --prefix jpeg-xl)'/lib' + export DEP_BROTLI_LIB=$(brew --prefix brotli)'/lib' + export DEP_HWY_LIB=$(brew --prefix highway)'/lib' + source venv/bin/activate pip install maturin maturin develop --features dynamic From 58f14c2bb0695f7bb9755b37921ecb2d12d76e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kerbiriou?= Date: Fri, 11 Oct 2024 05:10:34 +0200 Subject: [PATCH 2/4] Use the mode from the jpeg stream (#70) * disable jpeg encode warning when lossless_jpeg=True * use the mode from the reconstructed jpeg --- pillow_jxl/JpegXLImagePlugin.py | 27 +++++++++++++++++---------- test/test_plugin.py | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pillow_jxl/JpegXLImagePlugin.py b/pillow_jxl/JpegXLImagePlugin.py index 158738c..58f4e69 100644 --- a/pillow_jxl/JpegXLImagePlugin.py +++ b/pillow_jxl/JpegXLImagePlugin.py @@ -33,9 +33,15 @@ def _open(self): if self.jpeg: with Image.open(BytesIO(self._data)) as im: self._data = im.tobytes() - self._size = (self._jxlinfo.width, self._jxlinfo.height) - self.rawmode = self._jxlinfo.mode - self.info["icc_profile"] = icc_profile + self._size = im.size + self.rawmode = im.mode + self.info = im.info + icc_profile = im.info.get("icc_profile", icc_profile) + else: + self._size = (self._jxlinfo.width, self._jxlinfo.height) + self.rawmode = self._jxlinfo.mode + if icc_profile: + self.info["icc_profile"] = icc_profile # NOTE (Isotr0py): PIL 10.1.0 changed the mode to property, use _mode instead if parse(PIL.__version__) >= parse("10.1.0"): self._mode = self.rawmode @@ -98,7 +104,7 @@ def _save(im, fp, filename, save_all=False): effort = info.get("effort", 7) use_container = info.get("use_container", False) use_original_profile = info.get("use_original_profile", False) - jpeg_encode = info.get("lossless_jpeg", True) + jpeg_encode = info.get("lossless_jpeg", None) num_threads = info.get("num_threads", -1) enc = Encoder( @@ -113,12 +119,13 @@ def _save(im, fp, filename, save_all=False): ) # FIXME (Isotr0py): im.filename maybe None if parse stream # TODO (Isotr0py): This part should be refactored in the near future - if im.format == "JPEG" and im.filename and jpeg_encode: - warnings.warn( - "Using JPEG reconstruction to create lossless JXL image from JPEG. " - "This is the default behavior for JPEG encode, if you want to " - "disable this, please set 'lossless_jpeg' to False." - ) + if im.format == "JPEG" and im.filename and (jpeg_encode or jpeg_encode is None): + if jpeg_encode is None: + warnings.warn( + "Using JPEG reconstruction to create lossless JXL image from JPEG. " + "This is the default behavior for JPEG encode, if you want to " + "disable this, please set 'lossless_jpeg'." + ) with open(im.filename, "rb") as f: data = enc(f.read(), im.width, im.height, jpeg_encode=True) else: diff --git a/test/test_plugin.py b/test/test_plugin.py index a94db8a..66c0f28 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -32,7 +32,7 @@ def test_encode(image): def test_jpeg_encode(): temp = tempfile.mktemp(suffix=".jxl") img_ori = Image.open("test/images/sample.jpg") - img_ori.save(temp, lossless=True) + img_ori.save(temp, lossless=True, lossless_jpeg=True) img_enc = Image.open(temp) assert img_ori.size == img_enc.size == (40, 50) From 624a98c1d6a2a50acbad07a75953c9285db386d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kerbiriou?= Date: Fri, 11 Oct 2024 05:11:13 +0200 Subject: [PATCH 3/4] decode threads is a module constant (#76) Also adds num_threads to the signatures in pyi --- pillow_jxl/JpegXLImagePlugin.py | 3 ++- pillow_jxl/__init__.pyi | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pillow_jxl/JpegXLImagePlugin.py b/pillow_jxl/JpegXLImagePlugin.py index 58f4e69..705f557 100644 --- a/pillow_jxl/JpegXLImagePlugin.py +++ b/pillow_jxl/JpegXLImagePlugin.py @@ -8,6 +8,7 @@ from pillow_jxl import Decoder, Encoder _VALID_JXL_MODES = {"RGB", "RGBA", "L", "LA"} +DECODE_THREADS = -1 # -1 detect available cpu cores, 0 disables parallelism def _accept(data): @@ -26,7 +27,7 @@ class JXLImageFile(ImageFile.ImageFile): def _open(self): self.fc = self.fp.read() - self._decoder = Decoder() + self._decoder = Decoder(num_threads=DECODE_THREADS) self.jpeg, self._jxlinfo, self._data, icc_profile = self._decoder(self.fc) # FIXME (Isotr0py): Maybe slow down jpeg reconstruction diff --git a/pillow_jxl/__init__.pyi b/pillow_jxl/__init__.pyi index 055561d..fe4f9c7 100644 --- a/pillow_jxl/__init__.pyi +++ b/pillow_jxl/__init__.pyi @@ -15,7 +15,8 @@ class Encoder: parallel: bool = True, has_alpha: bool = False, lossless: bool = True, - quality: float = 0.0): ... + quality: float = 0.0, + num_threads: int = -1): ... def __call__(self, data: bytes, width: int, height: int, jpeg_encode: bool) -> bytes: ... ''' @@ -37,7 +38,7 @@ class Decoder: parallel(`bool`): enable parallel decoding ''' - def __init__(self, parallel: bool = True): ... + def __init__(self, num_threads: int = -1): ... def __call__(self, data: bytes) -> (bool, ImageInfo, bytes): ... ''' From 071bc7ca82d745ff2b8ce6deb67a7323ecb2647d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kerbiriou?= Date: Fri, 11 Oct 2024 05:12:01 +0200 Subject: [PATCH 4/4] don't store empty exif profiles (forcing the use of JXL container) (#77) Image.getexif().tobytes() always returns a non empty string. The truth value of the Exif object should be used instead. --- pillow_jxl/JpegXLImagePlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pillow_jxl/JpegXLImagePlugin.py b/pillow_jxl/JpegXLImagePlugin.py index 705f557..3579c13 100644 --- a/pillow_jxl/JpegXLImagePlugin.py +++ b/pillow_jxl/JpegXLImagePlugin.py @@ -130,7 +130,10 @@ def _save(im, fp, filename, save_all=False): with open(im.filename, "rb") as f: data = enc(f.read(), im.width, im.height, jpeg_encode=True) else: - exif = info.get("exif", im.getexif().tobytes()) + exif = info.get("exif") + if exif is None: + exif = im.getexif() + exif = exif.tobytes() if exif else None if exif and exif.startswith(b"Exif\x00\x00"): exif = exif[6:] metadata = {