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 for Windows + https://gitlares.github.io/fs-pdf-compressor/windows/ + Signed Windows x64 installer updates + en + + FS PDF Compressor 1.0.17 + Sat, 14 Nov 2026 03:41:26 +0000 + https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.17 + + + + \ No newline at end of file diff --git a/docs/appcast.xml b/docs/appcast.xml index 102b643..01cc105 100644 --- a/docs/appcast.xml +++ b/docs/appcast.xml @@ -4,6 +4,17 @@ FS PDF Compressor updates https://gitlares.github.io/fs-pdf-compressor/ Signed updates for FS PDF Compressor. + + 1.0.17 + Fri, 13 Nov 2026 21:39:42 -0600 + https://gitlares.github.io/fs-pdf-compressor/ + https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.17 + 1.0.17 + 1.0.17 + 14.0 + arm64 + + 1.0.16 Tue, 29 Sep 2026 09:15:41 -0600 diff --git a/docs/index.html b/docs/index.html index 78a2a1d..5f206dd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -79,10 +79,10 @@ "Linux x86_64" ], "softwareRequirements": "macOS 14+ on Apple Silicon, Windows 11 x64, or Linux x86_64", - "softwareVersion": "1.0.16", + "softwareVersion": "1.0.17", "dateModified": "2026-10-02", "downloadUrl": "https://github.com/gitlares/fs-pdf-compressor/releases/latest", - "releaseNotes": "https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.16", + "releaseNotes": "https://github.com/gitlares/fs-pdf-compressor/releases/tag/v1.0.17", "license": "https://github.com/gitlares/fs-pdf-compressor/blob/main/LICENSE", "isAccessibleForFree": true, "image": "https://gitlares.github.io/fs-pdf-compressor/og.png", @@ -167,15 +167,15 @@

Free PDF Compressor
for your desktop.

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.

- + Download for Apple Silicon - Download for Windows + Download for Windows Get it from Snap Store Linux AppImage View source
-

v1.0.16 · macOS 14+ Apple Silicon · Windows 11 x64 · Linux x86_64 via Snap or AppImage · Free forever

+

v1.0.17 · macOS 14+ Apple Silicon · Windows 11 x64 · Linux x86_64 via Snap or AppImage · Free forever