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
6 changes: 3 additions & 3 deletions .github/workflows/build-linux-appimage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
version:
description: Version matching the checked-out source
required: true
default: "1.0.16"
default: "1.0.17"

permissions:
contents: read
Expand Down Expand Up @@ -34,8 +34,8 @@ jobs:
mkdir -p .tools
curl --fail --location --retry 3 \
--output "$APPIMAGETOOL" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $APPIMAGETOOL" | sha256sum --check --status
https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 $APPIMAGETOOL" | sha256sum --check
chmod +x "$APPIMAGETOOL"
- name: Assemble AppImage
env:
Expand Down
27 changes: 23 additions & 4 deletions .github/workflows/build-macos14-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ on:
version:
description: Version matching the checked-out source
required: true
default: "1.0.16"
default: "1.0.17"

permissions:
contents: read

jobs:
assemble:
runs-on: macos-14
runs-on: macos-15
env:
APP_VERSION: ${{ inputs.version }}
SOURCE_REF: ${{ github.sha }}
Expand All @@ -23,12 +23,29 @@ jobs:
DIST_DIR: ci-build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
architecture: arm64
- name: Install Apple Silicon dependencies
run: |
brew install python@3.12 ghostscript
python3.12 -m venv .build-venv
brew install ghostscript
python -m venv .build-venv
.build-venv/bin/python -m pip install --upgrade pip
.build-venv/bin/python -m pip install -r requirements-build.txt
- name: Build macOS 14-compatible libidn
run: |
archive="$RUNNER_TEMP/libidn-1.44.tar.gz"
prefix="$RUNNER_TEMP/libidn-1.44-install"
curl --fail --location --retry 3 --output "$archive" \
https://ftp.gnu.org/gnu/libidn/libidn-1.44.tar.gz
echo "499608bab3a65650a0ea52888c13a8deebe3f71408e319acd9ec52e02eb13959 $archive" | shasum -a 256 --check
tar -xf "$archive" -C "$RUNNER_TEMP"
cd "$RUNNER_TEMP/libidn-1.44"
./configure --prefix="$prefix" --disable-shared --enable-static
make -j "$(sysctl -n hw.ncpu)"
make install
echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV"
- name: Build pinned Ghostscript 10.07.1
run: |
archive="$RUNNER_TEMP/ghostscript-10.07.1.tar.xz"
Expand All @@ -43,6 +60,8 @@ jobs:
./configure --prefix="$prefix" --disable-cups --disable-fontconfig --without-x
make -j "$(sysctl -n hw.ncpu)"
make install
# Make the resource tree available when the app sets GS_LIB.
make install-resdata0 install-iccdata0
cp doc/COPYING "$prefix/LICENSE"
test "$("$prefix/bin/gs" --version)" = "10.07.1"
echo "GHOSTSCRIPT_ROOT=$prefix" >> "$GITHUB_ENV"
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/build-snap-candidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Build Linux Snap candidate

on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/build-snap-candidate.yml"
- "snapcraft.yaml"
- "snap/**"
- "fs_pdf_compressor/**"
- "linux_app.py"

permissions:
contents: read

jobs:
assemble:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79
id: snapcraft
- name: Generate package checksum
env:
SNAP_FILE: ${{ steps.snapcraft.outputs.snap }}
run: |
cd "$(dirname "$SNAP_FILE")"
snap_name="$(basename "$SNAP_FILE")"
sha256sum "$snap_name" > "$snap_name.sha256"
- uses: actions/upload-artifact@v4
with:
name: fs-pdf-compressor-snap-amd64
path: |
${{ steps.snapcraft.outputs.snap }}
${{ steps.snapcraft.outputs.snap }}.sha256
if-no-files-found: error
retention-days: 7
26 changes: 24 additions & 2 deletions .github/workflows/build-windows-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
version:
description: Version matching the checked-out source
description: Windows version matching WINDOWS_APP_VERSION in the checked-out source
required: true

permissions:
Expand Down Expand Up @@ -60,6 +60,7 @@ jobs:
python -m venv .windows-build-venv
.windows-build-venv\Scripts\python -m pip install --upgrade pip
.windows-build-venv\Scripts\python -m pip install -r requirements-windows.txt
.windows-build-venv\Scripts\python -m pip install cryptography==46.0.3
$env:QT_QPA_PLATFORM = "offscreen"
.windows-build-venv\Scripts\python -B -m unittest discover -s tests
if ($LASTEXITCODE -ne 0) { throw "Windows regression tests failed" }
Expand All @@ -82,9 +83,30 @@ jobs:
"$app\ghostscript\bin\gswin64c.exe",
"$app\ghostscript\AGPL-3.0.txt",
"$app\SOURCE_OFFER.md",
"$app\THIRD_PARTY_MANIFEST.json"
"$app\THIRD_PARTY_MANIFEST.json",
"$app\WinSparkle.dll",
"$app\licenses\winsparkle\COPYING"
) | ForEach-Object { if (-not (Test-Path $_)) { throw "Missing runtime compliance file: $_" } }

- name: Validate installer upgrade and native signed updates
shell: pwsh
timeout-minutes: 12
run: |
$installer = Get-ChildItem ci-windows-build -Filter "*-setup.exe" | Select-Object -First 1
.windows-build-venv\Scripts\python -B -m scripts.verify_windows_installer --installer $installer.FullName --portable "$env:RUNNER_TEMP\candidate" --output "$env:RUNNER_TEMP\windows-validation"
if ($LASTEXITCODE -ne 0) { throw "Windows installer/update validation failed" }

- name: Upload validation evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: windows-validation-${{ inputs.version }}
path: |
${{ runner.temp }}/windows-validation/*.json
${{ runner.temp }}/windows-validation/*.log
if-no-files-found: warn
retention-days: 7

- name: Upload Windows release candidate
uses: actions/upload-artifact@v4
with:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 1.0.17 — 2026-11-13

- Detect malformed embedded colour profiles, page geometry changes, and altered selectable
text before replacing an original or saving a compressed copy.
- Retry eligible colour-profile failures with Ghostscript at the original
image resolution. Restore the original embedded ICC profile when unchanged
JPEGs and the surviving profiles identify it unambiguously.
- Add the optional Smart scans profile for monochrome documents.
Thanks to [@MicheleFerr](https://github.com/MicheleFerr) for
[PR #18](https://github.com/gitlares/fs-pdf-compressor/pull/18).
- Add WinSparkle to the Windows installer for signed update downloads and
user-approved installation. Portable updates remain manual. Existing
Windows users need one manual installation to get the updater.

## 1.0.16 — 2026-09-29

- Restore Ghostscript 10.07.1 on macOS, Windows, AppImage, and Snap after a
Expand Down
10 changes: 7 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ limited to software updates and links explicitly opened by the user:

- On macOS, Sparkle periodically reads the public update feed hosted on GitHub
Pages and can download signed update archives from GitHub Releases.
- On Windows, **Check for Updates…** opens the public GitHub Releases page in
the user's browser. The app does not check for or download updates in the
background.
- Windows installer builds with WinSparkle ask whether to check for updates
automatically. If enabled, they periodically read a public GitHub Pages feed.
**Check for Updates…** also checks that feed on request. Accepted updates are
downloaded from GitHub Releases and their Ed25519 signatures are verified
before installation. No document data is sent. Portable builds and the
published 1.0.16 Windows release open the GitHub Releases page for manual
updates instead.
- On Linux, the AppImage contacts the GitHub Releases API only when the user
selects **Check for Updates…**. If the user accepts an available update, it
downloads the AppImage and its published SHA-256 checksum from GitHub.
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ no document is uploaded.

- **macOS 14+ on Apple Silicon:** [download the signed and notarized DMG](https://github.com/gitlares/fs-pdf-compressor/releases/latest).
- **Linux x86_64:** [install from the Snap Store](https://snapcraft.io/fs-pdf-compressor) for automatic updates, or [download the self-contained AppImage](https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-x86_64.AppImage).
- **Windows 11 x64:** [download the per-user installer](https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-1.0.16-windows-x86_64-setup.exe) or the [portable ZIP](https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-1.0.16-windows-x86_64.zip).
- **Windows 11 x64:** [download the per-user installer](https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-1.0.17-windows-x86_64-setup.exe) or the [portable ZIP](https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-1.0.17-windows-x86_64.zip).

The macOS build is Developer ID signed and Apple-notarized, so it opens
normally with Gatekeeper enabled. The Linux AppImage is portable and bundles
Expand All @@ -114,10 +114,10 @@ user-writable location so in-app updates can replace it safely.

The Windows installer includes its dependencies and needs no administrator
rights. It is currently unsigned, so verify the matching published SHA-256
checksum before installing it. Windows updates are manual for now: download a
new installer from the official GitHub release when one is published.
checksum before installing it. Install 1.0.17 manually to enable future signed
updates through WinSparkle. Portable updates are manual.

See [what changed in 1.0.16](docs/releases/1.0.16.md) or read the complete
See [what changed in 1.0.17](docs/releases/1.0.17.md) or read the complete
[changelog](CHANGELOG.md).

## Build from source
Expand Down Expand Up @@ -151,7 +151,7 @@ check; see [Linux instructions](docs/LINUX.md).
The Windows edition uses the shared Qt interface and can be built from source
as an x86_64 per-user installer. See the [Windows build and test
instructions](docs/WINDOWS.md). The public installer and its portable ZIP are
available from the [1.0.16 release](https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.16).
available from the [1.0.17 release](https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.17).

## Code structure

Expand Down Expand Up @@ -196,7 +196,7 @@ delivery and links opened by the user; see the short
## License

FS PDF Compressor is released under the GNU Affero General Public License v3.0
or later. Version 1.0.16 bundles Ghostscript 10.07.1 under the AGPL and
or later. Version 1.0.17 bundles Ghostscript 10.07.1 under the AGPL and
other open-source libraries; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

Copyright © 2026 Daniel Lares.
Expand Down
5 changes: 5 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ The application and bundled Ghostscript remain available under AGPL-3.0-or-later

## Windows desktop runtime

Windows packages also include WinSparkle 0.9.4 (MIT) for signed installer
updates. Its license and bundled Expat license are included under
`_internal/licenses/winsparkle/`. Corresponding source:
https://github.com/vslavik/winsparkle/tree/v0.9.4

Windows uses PySide6 and the unmodified official AGPL Ghostscript distribution.
The pywin32 package provides access to the Windows Shell recycle operation;
its license text is copied from the installed distribution into the package's
Expand Down
4 changes: 3 additions & 1 deletion build_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import shutil
import subprocess
import sys
import sysconfig
import tarfile
import urllib.request
from pathlib import Path
Expand Down Expand Up @@ -387,7 +388,8 @@ def bundle_python_runtime_licenses() -> dict[str, str]:
python_executable = Path(sys.executable).resolve()
_copy_required_license(
destination / "Python-PSF-LICENSE.txt",
[parent / "LICENSE" for parent in python_executable.parents],
[Path(sysconfig.get_path("stdlib")) / "LICENSE.txt"]
+ [parent / "LICENSE" for parent in python_executable.parents],
)
_copy_required_license(
destination / "PyInstaller-GPL-2.0-with-exception.txt",
Expand Down
36 changes: 34 additions & 2 deletions build_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import shutil
import subprocess
import sys
import urllib.request
import zipfile
from importlib.metadata import version as package_version, distribution
from pathlib import Path

Expand All @@ -27,10 +29,35 @@
DIST = ROOT / os.environ.get("DIST_DIR", "release-windows")
BUILD = ROOT / ".windows-build"
APP_NAME = "FS PDF Compressor"
from fs_pdf_compressor.version import APP_VERSION
from fs_pdf_compressor.version import WINDOWS_APP_VERSION as APP_VERSION
from fs_pdf_compressor.windows_update import WINSPARKLE_VERSION
ARCHITECTURE = "x86_64"
GHOSTSCRIPT_VERSION = "10.07.1"
PACKAGE_NAME = f"FS-PDF-Compressor-{APP_VERSION}-windows-{ARCHITECTURE}"
WINSPARKLE_SHA256 = "6037df37fc263bd1650a1c4949681a9d40ffe991d01f35892a406cb5d103c976"


def bundle_winsparkle(resources: Path) -> None:
"""Download a pinned official archive and copy only the x64 runtime."""
cache = Path(os.environ.get("LOCALAPPDATA", str(Path.home() / ".cache"))) / APP_NAME
cache.mkdir(parents=True, exist_ok=True)
archive = cache / f"WinSparkle-{WINSPARKLE_VERSION}.zip"
if not archive.is_file():
url = f"https://github.com/vslavik/winsparkle/releases/download/v{WINSPARKLE_VERSION}/{archive.name}"
request = urllib.request.Request(url, headers={"User-Agent": "FS-PDF-Compressor-build"})
with urllib.request.urlopen(request, timeout=60) as response:
archive.write_bytes(response.read())
if hashlib.sha256(archive.read_bytes()).hexdigest() != WINSPARKLE_SHA256:
archive.unlink()
raise RuntimeError("WinSparkle archive checksum did not match")
with zipfile.ZipFile(archive) as source:
prefix = f"WinSparkle-{WINSPARKLE_VERSION}/"
resources.mkdir(parents=True, exist_ok=True)
(resources / "WinSparkle.dll").write_bytes(source.read(prefix + "x64/Release/WinSparkle.dll"))
licenses = resources / "licenses" / "winsparkle"
licenses.mkdir(parents=True, exist_ok=True)
for name in ("COPYING", "COPYING.expat"):
(licenses / name).write_bytes(source.read(prefix + name))


def run(*args: str) -> None:
Expand Down Expand Up @@ -160,7 +187,8 @@ def bundle_compliance_documents(resources: Path, ghostscript_version_value: str)
f"https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/{source_tag}/"
f"ghostpdl-{ghostscript_version_value}.tar.xz\n"
"PySide6 source: https://code.qt.io/pyside/pyside-setup\n"
"PyInstaller source: https://github.com/pyinstaller/pyinstaller\n",
"PyInstaller source: https://github.com/pyinstaller/pyinstaller\n"
f"WinSparkle {WINSPARKLE_VERSION} source: https://github.com/vslavik/winsparkle/tree/v{WINSPARKLE_VERSION}\n",
encoding="utf-8",
)
(resources / "THIRD_PARTY_MANIFEST.json").write_text(
Expand All @@ -177,6 +205,9 @@ def bundle_compliance_documents(resources: Path, ghostscript_version_value: str)
"ghostscript": ghostscript_version_value,
"ghostscript_license": "AGPL-3.0-or-later",
"ghostscript_modified": False,
"winsparkle": WINSPARKLE_VERSION,
"winsparkle_license": "MIT",
"winsparkle_source": f"https://github.com/vslavik/winsparkle/tree/v{WINSPARKLE_VERSION}",
},
indent=2,
sort_keys=True,
Expand Down Expand Up @@ -232,6 +263,7 @@ def main() -> None:
)
application = ROOT / "dist" / APP_NAME
resources = application / "_internal"
bundle_winsparkle(resources)
_, bundled_ghostscript_version = bundle_ghostscript(resources)
bundle_compliance_documents(resources, bundled_ghostscript_version)
archive_base = DIST / PACKAGE_NAME
Expand Down
Loading
Loading