Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

macos-postscript-print-fix

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.

Who needs this

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 / lpr from 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.

Features

  • 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

Contents

Check whether you are affected — read-only, changes nothing:

./scripts/diagnose.sh
macOS 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

Requirements

  • 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/urf or image/pwg-raster
  • Xcode Command Line Tools (otool, install_name_tool, codesign)

Installation

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 queues

install.sh patches every eligible queue by default. Name queues to be selective:

sudo ./scripts/install.sh Brother_HL_L2405W

Usage

Once installed, printing needs no further action — PostScript jobs from any source simply work:

lp -d Brother_HL_L2405W report.ps

VDI clients, scripts and legacy applications need no configuration; they print through the same queue as before.

Verify without printing

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.

Test print

Only after PASS, print a real test page:

./scripts/test-print.sh Brother_HL_L2405W

It cancels the job itself if the chain turns out wrong, so a misconfiguration cannot empty your paper tray.

Uninstall

sudo ./scripts/uninstall.sh

Removes the filter, restores every patched PPD, and leaves timestamped PPD backups in /private/etc/cups/ppd/.

How the bug works

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 cgpdftorasterrastertourf works
image/jpeg via raster filters works
application/postscript pstops → dead end
pstoappleps → 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.

How the fix works

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.

Why it is not simply "install Ghostscript"

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.

Why Apple's raster filters, not Ghostscript's

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.

What gets installed

/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.

Maintenance

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.sh

It 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.

Troubleshooting

Turn on debug logging, reproduce, then turn it back off:

sudo cupsctl LogLevel=debug

Print the failing job, then read what CUPS recorded:

grep -iE 'document-format|pstopdf-gs' /var/log/cups/error_log | tail -20
sudo cupsctl LogLevel=warn

A 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

Development

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 count

Syntax-check everything:

for f in filter/pstopdf-gs scripts/*.sh; do sh -n "$f" || echo "FAIL $f"; done

Contributing

Issues and pull requests welcome, particularly:

  • Results on other printers, especially image/pwg-raster queues
  • 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.

License

MIT — see LICENSE.

About

Restore PostScript printing on macOS for AirPrint/driverless printers. Fixes client-error-document-format-not-supported on Horizon/Citrix VDI redirection and lp/lpr jobs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages