Washed-Out Black: Why Windows Prints Worse Than Linux to the Same CUPS Queue
Same printer. Same CUPS queue. Same document.
From Linux: solid black text. From macOS: solid black text. From Windows 11: thin, grey, dotted — like the toner was almost gone. It wasn’t; the Linux prints from thirty seconds earlier proved that.
There is no darkness slider to fix this, and turning one up wouldn’t be the right fix anyway. The problem is which machine rasterizes the page.
The Setup
| Thing | Value |
|---|---|
| CUPS server | 192.168.10.42:631 |
| Queue name | HP_LaserJet_P1005 |
| CUPS driver | HP LaserJet P1005 Foomatic/foo2xqx |
| Client | Windows 11 Home |
The HP LaserJet P1005 is a host-based laser — a “winprinter”. It has no PostScript interpreter and no PCL interpreter. It understands exactly one thing: a proprietary raster stream called ZjStream. On the CUPS server, the foo2xqx filter is what produces that stream.
Which means something has to rasterize the page, and the only question that matters is what.
Two Paths to the Same Queue
%%{ init: { 'look': 'handDrawn' } }%%
graph TD
A[Application] --> B{Windows print driver}
B -->|Microsoft IPP Class Driver| C[Rasterize on the PC<br/>1-bit / grayscale bitmap<br/>anti-aliased edges]
B -->|Microsoft PS Class Driver| D[Emit PostScript<br/>text stays vector]
C --> E[CUPS foo2xqx]
D --> E
E --> F[ZjStream to printer]
C -.->|halftoned twice| G[Thin grey dots]
D -.->|halftoned once| H[Solid black]
When Windows auto-discovers a network printer, it installs the Microsoft IPP Class Driver. That driver renders the page to a bitmap on the PC — grayscale or 1-bit, with anti-aliased glyph edges — and ships the bitmap to CUPS. CUPS then halftones that bitmap again through foo2xqx.
Halftoning an already-anti-aliased bitmap is where the black goes. A thin stem of a letter that was rendered as a row of mid-grey anti-aliased pixels gets converted to a sparse scatter of dots. On screen the source bitmap looks fine; on paper the second halftone pass turns grey into holes.
Linux and macOS never hit this because they send PostScript or PDF — vector data, with the text still described as glyphs. foo2xqx rasterizes once, at the printer’s native 600 dpi, with no anti-aliasing to destroy.
| OS | What reaches the queue | Result |
|---|---|---|
| Linux / macOS | Vector PostScript or PDF | Solid black |
| Windows, IPP Class Driver | Pre-rasterized bitmap | Washed out |
| Windows, PS Class Driver | Vector PostScript | Solid black |
I did check for a settings fix first. The Microsoft IPP Class Driver exposes no darkness, quality, or EconoMode control — it offered grayscale and 600 dpi and nothing else. There is nothing to turn up. The washout is inherent to letting it pre-rasterize.
The Fix
Add a second Windows printer pointing at the same CUPS queue, driven by the inbox Microsoft PS Class Driver instead. It emits real PostScript, so the CUPS server does the rasterization — the exact path that already worked from Linux.
Both drivers ship with Windows. Nothing gets downloaded, and nothing changes on the CUPS server, so Linux and macOS printing is untouched.
Run this in an elevated Command Prompt (Start → type cmd → right-click → Run as administrator):
rundll32 printui.dll,PrintUIEntry /if /b "HP LaserJet P1005 (CUPS-PS)" /f "%WINDIR%\INF\prnms005.inf" /r "http://192.168.10.42:631/printers/HP_LaserJet_P1005" /m "Microsoft PS Class Driver"
| Flag | Meaning |
|---|---|
/if | Install a printer, staging the driver from the INF |
/b | The new printer’s display name |
/f | The inbox INF containing the PostScript class driver |
/r | The port — here the CUPS queue’s IPP URL |
/m | Force this driver instead of the auto-selected IPP one |
The reason this needs rundll32 rather than the Add Printer wizard: the GUI will not let you pair a manually chosen driver with an auto-discovered IPP port. It decides the driver for you. printui.dll does not.
Then set the new printer as default and delete the old washed-out entry.
Verifying
- Open
http://192.168.10.42:631/printers/and confirm the queue name and that its driver isFoomatic/foo2xqx— that tells you CUPS is doing the rendering. - Print the same black-text document to both printers.
- The
(CUPS-PS)one should have solid stems on lowercase letters. That is the tell — heavy glyphs look acceptable even on the broken path; thin strokes are where it shows.
The LPR Fallback
Pairing an http:// IPP port with a hand-picked driver is a little finicky, and on some Windows builds the port silently reverts. The robust alternative is LPR, which is fully supported by the GUI. It needs one thing enabled on the CUPS server:
sudo systemctl enable --now cups-lpd.socket
Then on Windows: Add Printer → Add a local printer → Create a new port → Standard TCP/IP Port → address 192.168.10.42, device type Custom → protocol LPR, queue name HP_LaserJet_P1005, and tick LPR Byte Counting Enabled. Assign the Microsoft PS Class Driver.
Byte counting matters: without it, some LPD implementations reject or truncate the job because the control file’s size field doesn’t match.
Worth knowing which ports you actually have open before choosing:
| Port | Protocol | State on my server |
|---|---|---|
| 631 | IPP | Open — the only one CUPS enables by default |
| 515 | LPD | Closed until cups-lpd.socket is enabled |
| 9100 | Raw / JetDirect | Closed — needs a separate listener |
I went with IPP on 631 because it was already open. If you enable LPD anyway, prefer it — it is the more reliable Windows→CUPS path.
Why Not Install the Real HP Driver?
Because there isn’t a useful one for this path. HP’s Windows driver for the P1005 produces ZjStream directly, aimed at a printer attached to that PC over USB. Pointed at a CUPS queue, you would be sending ZjStream into a filter chain that expects PostScript, and CUPS would either pass it through unrendered or reject it. The class driver is not a compromise here — for a shared host-based printer, PostScript-to-CUPS is the correct architecture.
The General Rule
For any host-based printer shared through CUPS, the rasterizer belongs on the CUPS server. Every client should send vector data and let one machine — the one that owns the printer-specific filter — do the conversion exactly once.
Windows breaks this by default, silently, and gives you no setting to correct it. The fix isn’t a quality slider; it’s changing what leaves the PC.
If your P1005 accepts jobs and prints nothing at all, that’s a different failure with a different cause — I wrote that one up separately in The Printer That Swallows Jobs.