Repository navigation
fix(compress): start the snap's gs; no pipe stalls, no Windows console - #194
Merged
Merged
Conversation
The snap stages the Ubuntu 22.04 ghostscript debs, which are built with --disable-compile-inits. Their gs looks for gs_init.ps under the compiled-in /usr/share/ghostscript/9.55.0, which inside the snap resolves against core22, where no Ghostscript data is installed. Measured with rev 43 on a core22 rootfs: gs exits 1 with "Can't find initialization file gs_init.ps" and _compress_pdf drops the Ghostscript pass without a message. Add app/_gs_paths.py, with no Qt imports. It reads the version from "gs --version" (5 s timeout, result cached per binary path) and, when <prefix>/share/ghostscript/<version>/Resource/Init exists under the binary's prefix, returns a copy of the environment with that directory and the sibling lib, when present, appended to GS_LIB. A GS_LIB the user set stays first, and os.environ is not modified. The directory is the one named after the binary's version because gs_init.ps aborts on a version mismatch. Without a <prefix>/share/ghostscript directory no probe runs and Popen gets env=None, as before. A failed probe (an exception, a non-zero exit, or output that is not a dotted version) is logged at WARNING and stays cached for the session, so a gs that hangs on --version costs one 5 s wait per session instead of one per compress run. That wait comes before the loop that polls for Cancel, so on the first compress Cancel can go unanswered for about 5 s; this is documented, not fixed. The probe and GS_LIB apply to any POSIX gs with a <prefix>/share/ghostscript directory, not only to the snap. Measured: on Fedora 44 and Arch that directory has no version subdirectory, so the probe runs and env stays None; on Debian 13 and Ubuntu 22.04, 24.04 and 26.04 the GS_LIB entries repeat the compiled-in search path and the result is the same; on Windows there is no share directory and no probe. macOS (Homebrew) was not measured. GS_LIB through env= rather than -I: gs 10.06.0 lists -I entries ahead of GS_LIB in its search path, so -I would put the derived entries ahead of a user's GS_LIB. Send the gs child's stdout and stderr to DEVNULL instead of PIPE. _compress_pdf polls the child and reads neither pipe, so a gs that writes more than a pipe buffer blocks until the 120 s deadline terminates it, and a working gs makes that reachable in the snap. On one malformed 400-page PDF the snap's gs 9.55.0 wrote 137600 bytes to stdout and none to stderr when drained. With PIPE it stopped with 64976 bytes in the 65536-byte pipe, _compress_pdf took about 120 s and gs ended with rc -15. Before GS_LIB, gs exited 1 at once; with DEVNULL it exited 0. Both runs finished within about a second. The gs output was not the result in any of the three: _compress_pdf keeps it only when gs exits 0, and with DEVNULL it (187468 bytes) was not the smallest. The result was 161585 -> 65124 bytes. With PIPE the native gs 9.55.0 of Ubuntu 22.04 also stalled for 120 s on this PDF. On the same PDF gs 10.06.0 and gswin64c 10.05.0 wrote 0 bytes to either stream. On Windows, start the gs child and the version probe with CREATE_NO_WINDOW. gswin64c is a console program and the windowed build has no console to share with it. Measured with gswin64c 10.05.0, running _compress_pdf under pythonw: without the flag every run opened a visible console window (Windows Terminal, the default terminal on the test machine): 3 of 3 with the old code, 3 of 3 with DEVNULL but without the flag. With it, none (3 of 3), with gs exiting 0 and the same final size. The probe does not run on that install. Measured with the real _compress_pdf on one 3-page PDF with 3000 px images and non-embedded Helvetica and Times-Roman: - snap rev 43 on core22: gs exits 0 at extreme (grayscale), recommended and low, embeds substitute fonts and keeps the text. Final size, old code -> this change: 3826561 -> 559297 bytes (recommended) and 33898872 -> 12556343 bytes (low). At extreme the PyMuPDF pass result (153514 bytes) is still the smallest and is kept. The same gs output and final sizes with DEVNULL as with PIPE. - /usr/bin/gs 10.06.0 on Ubuntu 26.04: same argv. The env differs only by GS_LIB, set to the first two entries of the compiled-in search path. With SOURCE_DATE_EPOCH set, the gs output matches the old code's except for the trailer /ID, which also differs between two runs of the old code. With DEVNULL, the gs output and final sizes match the old code's at all three levels. - gswin64c 10.05.0 on Windows: no share directory, no probe, env=None and the same argv. At all three levels the final sizes match the old code's, and so does the gs output except for its dates, document UUID and trailer /ID: gswin64c 10.05.0 ignores SOURCE_DATE_EPOCH, and those also differ between two runs of the old code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deploying pdfapps with
|
| Latest commit: |
3981818
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://348bdab9.pdfapps.pages.dev |
| Branch Preview URL: | https://fix-snap-ghostscript-lib.pdfapps.pages.dev |
CodeQL (py/uninitialized-local-variable) flagged `before` and `after` in test_gs_pass_finishes_when_gs_floods_stdout_and_stderr as possibly used before assignment: they are bound inside the try, and the except for CancelledError only calls pytest.fail, which CodeQL does not know always raises. The alerts are false positives, but unresolved review comments block the merge. Moving `assert after < before` into an else on the try makes the flow explicit: it runs only when _compress_pdf returned. What the test discriminates is unchanged; it still fails when gs gets a PIPE on stdout, stderr or both, and the AST differs only in where that assert sits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Compress in the snap silently skips its Ghostscript pass: the snap's gs
9.55.0 (Ubuntu 22.04 debs, built with --disable-compile-inits) looks for
gs_init.ps under the compiled-in /usr/share/ghostscript/9.55.0, which
inside the snap resolves against core22, where no Ghostscript data is
installed. gs exits 1 and _compress_pdf drops the pass without a message.
app/_gs_paths.py(new, no Qt): probesgs --version(5 s timeout,cached per binary) and, when
<prefix>/share/ghostscript/<version>/ Resource/Initexists, passes a copy of the environment with thatdirectory and its sibling
libappended toGS_LIB(a user'sGS_LIBstays first;os.environis untouched). The directory isthe binary's own version, because gs_init.ps aborts on a mismatch.
A failed probe is logged at WARNING.
_compress_pdf: gs stdout/stderr go to DEVNULL instead of anunread PIPE. With a working gs, a malformed PDF filled the pipe and
blocked gs until the 120 s deadline; the native gs 9.55.0 of Ubuntu
22.04 stalled the same way before this change.
windowed build no longer opens a console window per compress run.
Verification
_compress_pdf, 54 MB 3-page PDF): gs exits 0 at all three levels;final size, old -> new: recommended 3826561 -> 559297 bytes, low
33898872 -> 12556343 bytes; extreme keeps the smaller PyMuPDF result.
about a second and rc 0 with DEVNULL, same final result.
(gswin64c 10.05.0): same final sizes as before. macOS not measured.
none after.
Linux, the only survivor is equivalent on Windows (
;join whereos.pathsepis already;). Full suite on Windows: 846 passed,2 skipped.
Not exercised: real snapd confinement, macOS/Homebrew.
🤖 Generated with Claude Code