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
14 changes: 7 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
Expand All @@ -33,10 +33,10 @@ jobs:
os: [macos-14, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
Expand All @@ -62,10 +62,10 @@ jobs:
arch: apple-silicon
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
Expand All @@ -84,7 +84,7 @@ jobs:
# tested here — confirm the embedded interpreter starts and imports.
"$APP/Contents/MacOS/WebP Studio" --check
- run: ditto -c -k --keepParent "dist/WebP Studio.app" "WebP-Studio-macOS-${{ matrix.arch }}.zip"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: WebP-Studio-macOS-${{ matrix.arch }}
path: WebP-Studio-macOS-${{ matrix.arch }}.zip
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Release

on:
push:
tags: ["v*.*.*"]

# Only the publish job needs to write, to create the release and attach assets.
permissions:
contents: read

jobs:
macos-app:
strategy:
fail-fast: false
matrix:
include:
# py2app bundles the running interpreter, so a single runner produces
# a single-arch app that will not launch on the other kind of Mac.
- runner: macos-15-intel
arch: intel
- runner: macos-14
arch: apple-silicon
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
- run: |
pip install -r requirements.txt
pip install py2app
- run: python setup.py py2app
- name: Verify the bundle launches
run: |
APP="dist/WebP Studio.app"
test -d "$APP" || { echo "no bundle produced"; exit 1; }
"$APP/Contents/MacOS/WebP Studio" --check
- run: ditto -c -k --keepParent "dist/WebP Studio.app" "WebP-Studio-macOS-${{ matrix.arch }}.zip"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: WebP-Studio-macOS-${{ matrix.arch }}
path: WebP-Studio-macOS-${{ matrix.arch }}.zip
retention-days: 5

publish:
needs: macos-app
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
pattern: WebP-Studio-macOS-*
path: dist
merge-multiple: true
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
# Templated into the run script as literal text, so a tag crafted
# with shell metacharacters would be interpreted by bash. Passing
# it through env and referencing $TAG_NAME instead means the shell
# only ever sees it as a variable's value, never as script text.
TAG_NAME: ${{ github.ref_name }}
run: |
gh release create "$TAG_NAME" dist/*.zip \
--title "$TAG_NAME" \
--generate-notes
Comment thread
greptile-apps[bot] marked this conversation as resolved.
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,15 @@ python main.py --check

### Prebuilt macOS app

No signed releases yet — grab a build straight from CI instead:
[**Actions → Build → latest run**](https://github.com/DhakadG/ConvertImagesToWebP-MacApp/actions/workflows/build.yml)
→ pick `WebP-Studio-macOS-apple-silicon` or `WebP-Studio-macOS-intel` under
Artifacts. Unzip, then see [Gatekeeper](#the-app-is-damaged-and-cant-be-opened)
below before opening it. Artifacts expire after 14 days, so if the run has
aged out, use `workflow_dispatch` to trigger a fresh one from the Actions tab.
Grab the latest build from [**Releases**](https://github.com/DhakadG/ConvertImagesToWebP-MacApp/releases) —
pick `WebP-Studio-macOS-apple-silicon.zip` or `WebP-Studio-macOS-intel.zip`.
Unzip, then see [Gatekeeper](#the-app-is-damaged-and-cant-be-opened) below
before opening it. These are unsigned dev builds cut from tags, not
App Store releases.

No tagged release yet, or want the very latest commit instead? Grab a build
straight from CI: [**Actions → Build → latest run**](https://github.com/DhakadG/ConvertImagesToWebP-MacApp/actions/workflows/build.yml)
→ Artifacts. Those expire after 14 days; run `workflow_dispatch` for a fresh one.

## macOS

Expand Down Expand Up @@ -180,6 +183,18 @@ no window server, so that last step is yours.
Issues and PRs welcome. `python tests/test_engine.py` should stay green with
no display, and CI runs it plus the GUI boot suite on every PR.

### Cutting a release

Push a tag matching `v*.*.*`:

```bash
git tag v2.0.0
git push origin v2.0.0
```

`.github/workflows/release.yml` builds both macOS bundles and publishes them
to a new GitHub Release, titled after the tag with auto-generated notes.

## License

[MIT](LICENSE).
24 changes: 21 additions & 3 deletions core/imaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from core.config import Settings

# We routinely handle 100 MP camera scans; the decompression-bomb guard is for
# untrusted input, and these files come from the user's own disk.
Image.MAX_IMAGE_PIXELS = None
# Raised, not disabled: legitimate scans and panoramas run well past Pillow's
# ~89 MP default, but a folder can still contain a downloaded or crafted file
Comment thread
DhakadG marked this conversation as resolved.
# with an absurd declared size. Pillow itself only *warns* (doesn't raise)
# between 1x and 2x this value, so the real gate is the explicit check in
# convert_file below; this just keeps Pillow's own internal calls bounded too.
MAX_PIXELS = 300_000_000
Image.MAX_IMAGE_PIXELS = MAX_PIXELS

# Optional decoders. Both are pure-import side effects, so probe once at module
# load rather than per-file.
Expand Down Expand Up @@ -224,11 +228,25 @@ def _check_dimensions(width: int, height: int, output_format: str) -> None:
f"{limit}px — set a downscale limit, or choose PNG/AVIF")


def _check_pixel_count(width: int, height: int) -> None:
"""Pillow only warns, and still decodes, between 1x and 2x MAX_PIXELS —
this is the actual gate. Called right after Image.open(), before any
pixel data is touched, so an oversized file costs a header read, not a
full decode."""
pixels = width * height
if pixels > MAX_PIXELS:
raise ValueError(
f"{width}x{height} ({pixels:,} px) exceeds the {MAX_PIXELS:,} px "
f"safety limit — skipped before decoding")


def convert_file(source: Path, destination: Path, settings: Settings) -> Encoded:
"""Convert one image. Raises on failure — the runner turns that into a
per-file error row so one bad file can't abort the batch."""
note = ""
with Image.open(source) as opened:
_check_pixel_count(*opened.size)

if getattr(opened, "n_frames", 1) > 1:
note = "animated source, first frame only"

Expand Down
20 changes: 18 additions & 2 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from PIL import Image

from core.config import Settings
from core.imaging import (_check_dimensions, _target_size, available_output_formats,
convert_file)
from core.imaging import (MAX_PIXELS, _check_dimensions, _check_pixel_count,
_target_size, available_output_formats, convert_file)
from core.runner import CONVERTED, FAILED, SKIPPED, Runner, scan_sources, common_root


Expand Down Expand Up @@ -242,6 +242,21 @@ def test_webp_dimension_limit(tmp: Path):
print(" webp dimension limit ok")


def test_pixel_count_limit():
"""Pillow only warns (still decodes) between 1x and 2x MAX_IMAGE_PIXELS —
this must reject anything over the limit itself, not Pillow's fuzzed one."""
side = int(MAX_PIXELS ** 0.5)
_check_pixel_count(side, side) # just under the limit: fine

try:
_check_pixel_count(side + 1, side + 1) # inside Pillow's warn-only zone
except ValueError as exc:
assert str(MAX_PIXELS) in str(exc).replace(",", ""), exc
else:
raise AssertionError("image over MAX_PIXELS was not rejected")
print(" pixel count limit ok")


def test_format_falls_back_to_a_writable_one(tmp: Path):
"""A preset must not select a format this Pillow build cannot encode."""
settings = Settings.from_dict({"output_format": "avif"})
Expand Down Expand Up @@ -272,6 +287,7 @@ def main() -> int:
test_overwrite_keeps_good_output_when_encode_fails(tmp / "t11")
test_webp_dimension_limit(tmp / "t12")
test_format_falls_back_to_a_writable_one(tmp / "t13")
test_pixel_count_limit()
print("\nall engine checks passed")
return 0

Expand Down
Loading