Choose your package
Use Snap for managed updates or keep the AppImage in a user-writable location for a portable installation.
diff --git a/.github/workflows/build-linux-appimage.yml b/.github/workflows/build-linux-appimage.yml
index ef61a95..4d2c28f 100644
--- a/.github/workflows/build-linux-appimage.yml
+++ b/.github/workflows/build-linux-appimage.yml
@@ -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
@@ -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:
diff --git a/.github/workflows/build-macos14-arm64.yml b/.github/workflows/build-macos14-arm64.yml
index f331187..41bbf21 100644
--- a/.github/workflows/build-macos14-arm64.yml
+++ b/.github/workflows/build-macos14-arm64.yml
@@ -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 }}
@@ -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"
@@ -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"
diff --git a/.github/workflows/build-snap-candidate.yml b/.github/workflows/build-snap-candidate.yml
new file mode 100644
index 0000000..c667ce7
--- /dev/null
+++ b/.github/workflows/build-snap-candidate.yml
@@ -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
diff --git a/.github/workflows/build-windows-candidate.yml b/.github/workflows/build-windows-candidate.yml
index d04bdcd..f1dbe40 100644
--- a/.github/workflows/build-windows-candidate.yml
+++ b/.github/workflows/build-windows-candidate.yml
@@ -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:
@@ -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" }
@@ -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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27d51fa..847199a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/PRIVACY.md b/PRIVACY.md
index a91e94b..dd5bef6 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -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.
diff --git a/README.md b/README.md
index 4dcf629..fc380e3 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
@@ -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
@@ -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.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index a74b8f4..8d5676d 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -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
diff --git a/build_macos.py b/build_macos.py
index 7748143..424afb8 100644
--- a/build_macos.py
+++ b/build_macos.py
@@ -24,6 +24,7 @@
import shutil
import subprocess
import sys
+import sysconfig
import tarfile
import urllib.request
from pathlib import Path
@@ -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",
diff --git a/build_windows.py b/build_windows.py
index 4df446b..62d7c8d 100644
--- a/build_windows.py
+++ b/build_windows.py
@@ -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
@@ -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:
@@ -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(
@@ -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,
@@ -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
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index 4bd9c9f..0d6f359 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -132,7 +132,7 @@ automatically. Store credentials must remain outside the repository.
### Windows 11 x64 releases
Run **Build Windows x64** from the exact tag being released and enter the same
-version stored in `fs_pdf_compressor/version.py`. The workflow rejects a
+Windows version (`WINDOWS_APP_VERSION`) stored in `fs_pdf_compressor/version.py`. The workflow rejects a
version that does not match the checked-out source and produces these files:
```text
@@ -225,6 +225,61 @@ at its GitHub URL.
## Release compliance
+### Windows signed updates
+
+Windows installer builds bundle the official WinSparkle 0.9.4 x64 DLL. The
+builder verifies the pinned archive SHA-256 before copying it. The separate
+`docs/appcast-windows.xml` feed is initially empty; never add unsigned updates.
+The portable ZIP keeps manual updates. Only the executable whose directory
+matches the installer's HKCU `Software\gitlares\FS PDF Compressor\InstallDir`
+uses WinSparkle. Existing releases need one manual installer update to gain
+this integration.
+
+WinSparkle asks whether to check automatically on first launch and retains the
+user's choice. Automatic checks use a daily interval; installation requires the
+user's approval in its update dialog. A busy application rejects installation,
+including file discovery and pending Explorer requests. Once installation is
+approved while idle, new compression work is blocked until shutdown. Inno Setup
+updates the existing installation and relaunches the app.
+
+Updates use the existing Sparkle Ed25519 signing identity in the macOS login
+Keychain. Only its public verification key is embedded in the Windows app.
+This update signature does not provide Authenticode signing or remove Windows
+SmartScreen warnings.
+
+After completing the Windows 11 release checklist and uploading the exact
+installer plus SHA-256 file to its matching GitHub Release, run from the repo
+root with a Python environment containing `cryptography`:
+
+```sh
+python3 -m scripts.generate_windows_appcast 1.0.16.1 \
+ release-windows/FS-PDF-Compressor-1.0.16.1-windows-x86_64-setup.exe
+```
+
+The generator checks the published checksum, signs locally using Sparkle's
+`sign_update`, and verifies the signature against the Windows app's public key.
+It does not export private keys or publish the feed. Review and publish
+`docs/appcast-windows.xml` only after the release assets exist.
+
+Before publishing the first enabled release, test on Windows 11 x64: first-run
+automatic-check permission and persistence, manual update checks, no-update
+behavior, an actual signed upgrade to a newer test version, rejected tampered
+installer, installation deferred during discovery/compression, application
+shutdown/relaunch, preserved Keep original and Drop Zone preferences, Explorer
+integration, and portable ZIP manual-update behavior. Verify `_internal` contains
+`WinSparkle.dll` and `licenses/winsparkle/COPYING`. Do not claim end-to-end
+Windows updates based on unit tests or a macOS-only run.
+
+For a Windows-only maintenance revision, keep `APP_VERSION` at the shared
+product version and increment `WINDOWS_APP_VERSION` (for example, 1.0.16.1).
+Build from its dedicated source tag `v1.0.16.1-windows` and use 1.0.16.1 as the
+Windows workflow input. After testing, publish a Windows-only GitHub Release
+with `make_latest=false`, containing only its installer, portable ZIP and
+checksums. Do not mark it as a prerelease or replace the general release.
+The feed generator uses that Windows-specific tag for four-part versions.
+Update Windows download links to that exact release after publication; keep
+macOS/Linux links and `docs/appcast.xml` on the existing shared release.
+
Before publishing, create and push the Git tag named by `SOURCE_REF`. Attach
the platform packages to the matching GitHub Release instead of committing
binaries to Git. Every published package must be produced from that release
diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md
index 0da897a..706d515 100644
--- a/docs/WINDOWS.md
+++ b/docs/WINDOWS.md
@@ -1,13 +1,13 @@
# Build FS PDF Compressor on Windows
FS PDF Compressor can be built on Windows from this repository. The Windows
-edition uses the same local compression engine and three quality profiles as
+edition uses the same local compression engine and quality profiles as
the macOS and Linux editions. It is distributed under AGPL-3.0-or-later.
## Install the public build
For Windows 11 x64, download the per-user installer or portable ZIP from the
-[1.0.16 release](https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.16).
+[1.0.17 release](https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.17).
Each artifact has a matching SHA-256 file. The installer includes the
unmodified AGPL Ghostscript runtime and needs no administrator rights.
@@ -88,13 +88,14 @@ Install the setup executable on a clean Windows VM and check:
6. With **Keep original** on, the original remains beside
`name compressed.pdf`.
-Windows does not yet provide an in-app auto-update mechanism. Obtain new
-versions from the project's GitHub releases after they are published.
+Install 1.0.17 manually to enable future WinSparkle updates. Portable
+updates are manual. See the
+[update release checklist](WINDOWS_TESTING.md#winsparkle-update-checklist).
## Reproducible release builds
For ordinary public releases, build from the exact Git tag that defines the
-shared version used by macOS, Linux, and Windows. Set `APP_VERSION` and
+Windows version in `WINDOWS_APP_VERSION`. Set the build environment's `APP_VERSION` and
`SOURCE_REF` to that version/tag when producing the installer.
The **Build Windows x64** GitHub Actions workflow is the preferred packaging
@@ -103,6 +104,8 @@ version. Download the resulting installer, portable ZIP, and SHA-256 files,
then complete [Windows release testing](WINDOWS_TESTING.md) before attaching
them to the matching GitHub Release.
-Version 1.0.13 introduced the first Windows package. Current shared releases
-must build every published platform from the same version tag and identify the
-exact public source commit in their release notes.
+Windows-only revisions use a four-part version such as 1.0.16.1, with source
+tag `v1.0.16.1-windows`. They do not change the shared macOS/Linux version.
+Publish them as Windows-only releases without making them the general latest
+release; see [release instructions](RELEASING.md#windows-signed-updates).
+Every package must identify the exact public source commit used to build it.
diff --git a/docs/WINDOWS_TESTING.md b/docs/WINDOWS_TESTING.md
index 63179b2..9099a0e 100644
--- a/docs/WINDOWS_TESTING.md
+++ b/docs/WINDOWS_TESTING.md
@@ -50,3 +50,42 @@ installer itself has a problem:
Record the Windows edition, architecture, Ghostscript version, and any
SmartScreen message with the test result. Do not publish either package until
the checklist succeeds.
+
+## WinSparkle update checklist
+
+For the first installer release with WinSparkle, use two successive candidate
+versions and a test feed signed with the embedded verification key:
+
+1. Confirm `_internal/WinSparkle.dll` and its license texts are packaged.
+2. Confirm the first-run automatic-check choice persists after restarting.
+3. Run **Check for Updates…** against a feed with no newer release.
+4. Offer a newer signed installer and verify download, installation in the
+ existing directory, shutdown, and relaunch without duplicate instances.
+5. Confirm Keep original, Drop Zone, and Explorer integration survive upgrading.
+6. Change the downloaded installer bytes and verify installation is rejected.
+7. Attempt installation while discovering a folder or compressing a batch;
+ verify the current work is preserved and installation is refused until idle.
+8. Once installation starts, verify new drops and **Again** cannot start work.
+9. Unpack the portable ZIP in another directory and confirm it uses the manual
+ release-page link and never runs the installed application's updater.
+
+See [signed Windows updates](RELEASING.md#windows-signed-updates) for feed
+generation. The Ed25519 update signature does not replace Authenticode; the
+Windows installer remains unsigned unless separately code-signed.
+
+## Automated validation recorded on 2026-10-08
+
+[GitHub Actions run 37840732060](https://github.com/gitlares/fs-pdf-compressor/actions/runs/37840732060)
+passed for source commit `851db786106137e7a1424ca00553750114611813` on
+Windows Server 2022. It built the Windows 1.0.16.1 installer and portable ZIP,
+verified package contents, used the real WinSparkle DLL to authenticate a
+download and reject a tampered payload, checked an empty update feed, upgraded
+the published 1.0.16 installer using the native-verified payload, verified
+preserved preferences and Explorer registration, and checked installed
+application startup. See the [permanent result](validation/windows-1.0.16.1-ci.json).
+
+The native download test used a disposable signing key and a loopback feed.
+It did not use the production signing key or publish an update. Unattended
+installation and process startup do not replace visual review of the update
+approval dialog, relaunch, actual Explorer action, drag/drop, or Windows 11
+desktop behavior. Those checks remain pending before a public release.
diff --git a/docs/app.js b/docs/app.js
index 866b84c..1f0c85e 100644
--- a/docs/app.js
+++ b/docs/app.js
@@ -1,7 +1,7 @@
const fallback = {
- version: "v1.0.16",
- macosUrl: "https://github.com/gitlares/fs-pdf-compressor/releases/download/v1.0.16/FS-PDF-Compressor-1.0.16-arm64.dmg",
- windowsUrl: "https://github.com/gitlares/fs-pdf-compressor/releases/download/v1.0.16/FS-PDF-Compressor-1.0.16-windows-x86_64-setup.exe",
+ version: "v1.0.17",
+ macosUrl: "https://github.com/gitlares/fs-pdf-compressor/releases/download/v1.0.17/FS-PDF-Compressor-1.0.17-arm64.dmg",
+ windowsUrl: "https://github.com/gitlares/fs-pdf-compressor/releases/download/v1.0.17/FS-PDF-Compressor-1.0.17-windows-x86_64-setup.exe",
linuxUrl: "https://github.com/gitlares/fs-pdf-compressor/releases/latest/download/FS-PDF-Compressor-x86_64.AppImage",
};
diff --git a/docs/appcast-windows.xml b/docs/appcast-windows.xml
new file mode 100644
index 0000000..d5746a0
--- /dev/null
+++ b/docs/appcast-windows.xml
@@ -0,0 +1,15 @@
+
+
FS PDF Compressor is a free, open-source, offline PDF compressor for macOS Apple Silicon, Windows, and Linux. FS means Fast & Simple: drop in a PDF and get a smaller one—without uploads, accounts, or complicated settings. Compress PDFs without uploading them, with a signed and notarized Mac app, Windows installer, Snap Store install, or self-contained Linux AppImage.
Available now · Version 1.0.16
-Available now · Version 1.0.17
+This corrective release restores the proven Ghostscript 10.07.1 engine after a 10.08.0 regression was found to cover visible content in some Photoshop-generated PDFs. The three quality profiles and local-only workflow remain unchanged.
+FS PDF Compressor now checks embedded color profiles, page geometry, and selectable text before saving a compressed PDF.
Free and open source under AGPL-3.0-or-later.
Linux · x86_64 · Local only
Compress PDFs locally with a focused desktop interface. Install from Snap Store for automatic updates, or download the self-contained AppImage for a portable setup.
Linux · x86_64 · Local only
Compress PDFs locally with a focused desktop interface. Install from Snap Store for automatic updates, or download the self-contained AppImage for a portable setup.
Use Snap for managed updates or keep the AppImage in a user-writable location for a portable installation.
The per-user AppImage installer can add compression actions to GNOME Files and KDE Dolphin.
Keep original creates a separate copy and remembers your choice after you restart the app.
macOS · Apple Silicon · Local only
FS PDF Compressor makes PDFs smaller entirely on your Mac. Drop in a file, a batch, or a folder and choose Preserve, Balanced, or Maximum. There are no uploads, accounts, analytics, or subscriptions.
- - + +