Restore PostScript printing on macOS for AirPrint / driverless printers.
Modern macOS has no PostScript rasterizer. Send a PostScript job to a driverless printer and CUPS rejects it outright:
client-error-document-format-not-supported for Send-Document
The job lands as 0 bytes, the printer stays idle, and nothing explains why. The same printer prints fine from Windows and Linux, and fine from macOS apps — because those paths never produce PostScript.
This repo installs a CUPS filter that supplies the missing conversion step, so PostScript jobs print again.
You are likely affected if a printer shows up correctly but PostScript jobs vanish silently:
- VDI printer redirection — Omnissa Horizon, VMware Horizon, Citrix — where a remote Windows desktop prints back to a local Mac printer
lp/lprfrom scripts, or any tool emitting.ps- Legacy or scientific applications that print PostScript
- Unix tooling (
enscript,a2ps,groff -Tps,gnuplot)
Most affected printers are recent inkjets and entry-level lasers — Brother, Canon, HP, Epson — that ship as AirPrint-only with no vendor driver.
- One read-only command tells you whether you are affected
- Self-contained Ghostscript bundle — no runtime dependency on Homebrew
- Works inside the CUPS filter sandbox; nothing is disabled or weakened
- Chain verification that uses no paper, plus a guarded test print
- Patches all eligible queues automatically, or only the ones you name
- Clean uninstall with timestamped PPD backups
- Requirements
- Installation
- Usage
- Uninstall
- How the bug works
- How the fix works
- What gets installed
- Maintenance
- Troubleshooting
- Development
- Contributing
- License
Check whether you are affected — read-only, changes nothing:
./scripts/diagnose.shmacOS 15.6 (arm64)
PostScript rasterizer in ServerBin: ABSENT -- PostScript cannot reach raster on its own
== Brother_HL_L2405W
printer-ready format: image/urf
filter installed: no
PostScript accepted: NO <-- affected
- macOS 13 or newer (developed and tested on 15.7, Apple Silicon)
- Homebrew Ghostscript — build time only; the installed filter carries its own copy
- A printer whose queue uses
image/urforimage/pwg-raster - Xcode Command Line Tools (
otool,install_name_tool,codesign)
git clone https://github.com/ShaneBreazeale/macos-postscript-print-fix.git
cd macos-postscript-print-fix
brew install ghostscript # skip if already installed
./scripts/bundle-gs.sh # builds a relocatable copy, no sudo
sudo ./scripts/install.sh # installs it and patches your queuesinstall.sh patches every eligible queue by default. Name queues to be
selective:
sudo ./scripts/install.sh Brother_HL_L2405WOnce installed, printing needs no further action — PostScript jobs from any source simply work:
lp -d Brother_HL_L2405W report.psVDI clients, scripts and legacy applications need no configuration; they print through the same queue as before.
Check the filter chain without using paper:
sudo ./scripts/verify-chain.sh Brother_HL_L2405W==> printer-ready format for Brother_HL_L2405W is image/urf
==> filter chain CUPS chose:
2 filters for job:
/Library/Printers/PSFilter/filter/pstopdf-gs (application/postscript to printer/…/image/urf, cost 50)
- (printer/…/image/urf to printer/…, cost 0)
PASS: printer-ready format is image/urf -- safe to print for real.
Only after PASS, print a real test page:
./scripts/test-print.sh Brother_HL_L2405WIt cancels the job itself if the chain turns out wrong, so a misconfiguration cannot empty your paper tray.
sudo ./scripts/uninstall.shRemoves the filter, restores every patched PPD, and leaves timestamped PPD
backups in /private/etc/cups/ppd/.
A driverless queue's PPD declares exactly one printer-ready format:
*cupsFilter2: "image/urf image/urf 10 -"
CUPS computes document-format-supported by walking its MIME graph to see
which input types can reach that format:
| Input | Path to image/urf |
Result |
|---|---|---|
application/pdf |
cgpdftoraster → rastertourf |
works |
image/jpeg |
via raster filters | works |
application/postscript |
pstops → dead endpstoappleps → dead end |
unreachable |
macOS ships pstops and pstoappleps, but neither leads to raster — Apple
removed Ghostscript years ago and never replaced the PostScript rasterizer.
PostScript is therefore excluded from the supported list, and CUPS rejects the
job at IPP time, before any filter runs.
Windows and Linux are unaffected because their spoolers still ship a PostScript rasterizer.
One filter that converts PostScript to the printer's raster format, declared in the queue's PPD:
*cupsFilter2: "application/postscript image/urf 50 /Library/Printers/PSFilter/filter/pstopdf-gs"
Internally:
postscript → gs (pdfwrite) → pdf → cgpdftoraster → cups-raster → rastertourf → urf
Adding that single graph edge makes PostScript reachable, so
document-format-supported picks it up and jobs stop being rejected.
Three macOS constraints shape the design. Each was a dead end first.
SIP. The canonical filter directory /usr/libexec/cups/filter sits on the
sealed system volume — unwritable even as root. The filter cannot go where CUPS
would normally look for it.
The filter sandbox. cupsd runs each filter under a per-job sandbox that
permits /Library/Printers but not /usr/local or /opt/homebrew. A filter in
/usr/local dies with Operation not permitted; one that merely calls
Homebrew's gs fails the same way. Hence bundle-gs.sh: it copies gs with its
full dylib closure into /Library/Printers, rewrites every install name to
@loader_path, and re-signs each Mach-O — arm64 refuses to load a modified
binary carrying a stale signature.
cupsFilter2 semantics. Its destination declares the printer-ready
format, not a pipeline stage. Naming application/pdf there tells CUPS that PDF
needs no further processing, so it skips rasterization and sends raw PDF to a
printer that only speaks URF — which prints stacks of blank pages. A .convs
rule would express an intermediate step correctly, but CUPS resolves .convs
filter names against ServerBin, which SIP blocks. So the filter has to carry
the job all the way to the printer's format itself.
Ghostscript has a urfgray device that would remove a pipeline stage, but it
writes a page count of 0 in the URF header:
00000000: 554e 4952 4153 5400 0000 0000 UNIRAST..... ← gs urfgray: 0 pages
00000000: 554e 4952 4153 5400 0000 0001 UNIRAST..... ← rastertourf: 1 page
Printers render nothing from the first. Routing through cgpdftoraster and
rastertourf reuses the exact code path macOS already uses for ordinary PDF
printing, so page geometry, resolution and color handling match what the
printer expects.
/Library/Printers/PSFilter/filter/pstopdf-gs the filter
/Library/Printers/PSFilter/{bin,lib,share} bundled Ghostscript (~48 MB)
/private/etc/cups/ppd/<queue>.ppd one added cupsFilter2 line
/private/etc/cups/ppd/<queue>.ppd.bak-* timestamped backups
Nothing else on the system is modified. The CUPS sandbox stays enabled.
A macOS update, or re-adding the printer, regenerates the PPD and drops the filter line — printing breaks again the same silent way. Re-run:
sudo ./scripts/install.shIt reuses the already-installed Ghostscript bundle, so this works even if
Homebrew's ghostscript is long gone. Run bundle-gs.sh only to rebuild from
scratch — the bundle is a frozen copy that brew upgrade does not touch, so
rebuild it periodically to pick up Ghostscript security fixes.
Turn on debug logging, reproduce, then turn it back off:
sudo cupsctl LogLevel=debugPrint the failing job, then read what CUPS recorded:
grep -iE 'document-format|pstopdf-gs' /var/log/cups/error_log | tail -20
sudo cupsctl LogLevel=warnA successful job logs nothing — CUPS buffers per-job debug output and only
writes it to error_log if the job fails. Silence means it worked.
| Symptom | Cause |
|---|---|
client-error-document-format-not-supported |
filter not installed, or PPD regenerated |
Operation not permitted, status 126 |
filter outside a sandbox-permitted path |
Filter exits 1, missing bundled Ghostscript |
bundle not installed; run bundle-gs.sh then install.sh |
| Blank pages | printer-ready format wrong — run verify-chain.sh |
| Job format is not PostScript | the sender emits something else; check the document-format line |
The filter is POSIX shell and runs standalone, which makes it testable without CUPS:
./scripts/bundle-gs.sh
export PPD=/private/etc/cups/ppd/<queue>.ppd
PSFILTER_BASE="$PWD/gsbundle" ./filter/pstopdf-gs 1 user title 1 "" test.ps > out.urf
head -c 12 out.urf | xxd # expect UNIRAST and a nonzero page countSyntax-check everything:
for f in filter/pstopdf-gs scripts/*.sh; do sh -n "$f" || echo "FAIL $f"; doneIssues and pull requests welcome, particularly:
- Results on other printers, especially
image/pwg-rasterqueues - Results on Intel Macs and on macOS 13/14
- A cleaner mechanism than patching PPDs
Please include ./scripts/diagnose.sh output and your macOS version.
MIT — see LICENSE.