Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Maximize document quality and readability when faxing a PDF, Word, PowerPoint, Excel, or image file — make it arrive LEGIBLE after a noisy 1-bit Group-3 transmission (a fax's whole job is to be read), then optionally transmit it via a cloud fax API. Use to make a document faxable, convert it to bilevel, fix a muddy or unreadable fax, keep small text and signatures readable, halftone photos, or send a fax. Triggers: "fax this Word doc", "prep these scans to fax", "send this as a fax".
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 222% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 133% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 121% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 252% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 175% | 0% |
This skill converts a PDF into a fax-ready file and, above all, one that arrives LEGIBLE on the receiving machine.
Legibility is the entire objective. A fax exists to be read on the other end, often after surviving a low-resolution, 1-bit, lossy-by-design Group-3 transmission over a noisy phone line. So this skill is built to maximize document quality and enhance reception/readability — keep text crisp, small fonts and signatures intact, and photos recognizable — first. Smaller files and faster transmission are welcome side effects, never the goal: a tiny fax that arrives unreadable is a failure. When a trade-off is forced, choose what stays readable on the receiving machine.
> Just need to shrink a PDF for email or the web? That's a different job > with the opposite trade-offs — use the companion skill instead: > pdf-email-optimizer.
Verify the environment once:
bashpython3 -m pdf_fax_optimizer.check_deps # detect-only; prints the pip command # then, if anything is missing: pip install pdf-fax-optimizer # or: pip install -r requirements.txt
Requires Python 3.10+ with PyMuPDF (fitz), Pillow, numpy, opencv-python, and img2pdf. Sending a fax additionally needs the requests package; faxing Office/OpenDocument files (Word/PowerPoint/Excel) needs LibreOffice (headless); the OCR-driven text-polarity passes (--ocr-text and --recover-text) need rapidocr-onnxruntime (self-contained, no system binary). Without OCR the skill still works — it falls back to the binarizer's default black-on-white for document text and skips the within-image recover-text recolor. No CLI tools are required for PDF/image input. Verify deps with python3 -m pdf_fax_optimizer.check_deps; install with pip install pdf-fax-optimizer (add the [ocr,send] extras for OCR and cloud-fax sending) or pip install -r requirements.txt.
Read references/fax-optimization.md before running — it explains the constraints below and why each knob exists. Do not skip it; the defaults only make sense once you understand the Group-3 transmission model.
The goal of every choice here is a document that arrives readable. Each knob exists to protect legibility against a lossy channel — keeping edges sharp, rescuing faint strokes, and choosing halftones that don't collapse to mud after the receiving machine re-thresholds. Optimize for what the recipient can read, not for the smallest byte count.
The core constraint: Group-3 fax is 1-bit bilevel (pure black/white, no gray), compressed with run-length codes (CCITT G3/G4) along each scanline. That last fact is the hinge for every halftone decision — anything that creates many black↔white transitions per line compresses badly, transmits slowly, and is more vulnerable to line noise. Fax mode is fundamentally a balance between visual fidelity and transition density.
The pipeline runs at the source's NATIVE resolution with SQUARE pixels, hard-capped at 300 PPI — raster input (a PNG/JPG, a scanned page) comes through pixel-for-pixel below the ceiling and bicubic-downsamples to 300 PPI above it, never upscaled, never anisotropically stretched. 300 PPI is the legibility plateau of the 1-bit fax channel, and a square 300-PPI grid keeps page buffers at ~8 MP for letter-size. (Background: classic fax is anisotropic — a fine preset is 196 lines-per-inch vertical, super-fine 391 lpi vertical — but this pipeline renders a square grid whose 300 PPI exceeds both of those vertical line rates, so dropping the stretched axis costs nothing the channel could carry.) Upscaling a wrapped raster bicubic-interpolates its baked-in text and soft letterforms break the binarizer ("ABC" reads as "A8C"); a raster source's text quality is fundamentally bounded by its own pixel grid, so the pipeline honours that exactly. The --fax-resolution preset (fine / superfine, default superfine) sets the halftone screen detail and the rasterization DPI for vector content (whose glyphs are real vectors the renderer can rasterise crisp at any DPI), clipped to the 300-PPI ceiling. On a mixed page — live vector text plus an embedded low-DPI raster — the preset acts as a floor for the live text, so a single 72-DPI image can no longer drag the whole page (and its crisp body text) down to 72 PPI; a higher-DPI embedded raster can still pull the page up toward the 300 cap. At low native DPI the recommender auto-picks an FM screen (blue-noise / atkinson) — its dot pitch is the pixel itself, with no multi-pixel cell, so the screen looks fine and organic where a clustered cell would collapse to a chunky 24-lpi magazine pitch. Pass --transmission-safe to clamp the final output back to a 1728-px-wide Group-3 scanline if you need to send through a real fax machine.
Run it:
bashpython3 -m pdf_fax_optimizer.optimize_pdf INPUT.pdf -o OUTPUT.pdf \ --sample 1 --report OUTPUT.report.json # superfine + preserve_text; OCR recover_text is opt-in
INPUT doesn't have to be a PDF. The optimizer normalizes other formats to PDF first (via pdf_fax_optimizer.to_pdf), then runs the identical pipeline:
.png/.jpg/.tif/.bmp/.gif/.webp) — wrapped to PDF with img2pdf(no extra tools).
.doc/.docx/.rtf/.odt/.txt, .ppt/.pptx/.odp,.xls/.xlsx/.ods/.csv) — rendered to PDF by LibreOffice headless (soffice). If LibreOffice isn't installed the skill says so; install it (no GUI needed) or have the user export to PDF. --keep-converted-pdf keeps the intermediate PDF beside the output.
bashpython3 -m pdf_fax_optimizer.optimize_pdf proposal.docx -o proposal.fax.pdf # Word → fax PDF
What the pipeline does, per page (details in the reference):
input (a PNG/JPG, a scanned page) renders at its native pixels-per-inch exactly when below the ceiling — pixel-for-pixel with the source, no bicubic upsample to "lift" it — and downsamples cleanly to 300 PPI above it. Vector pages rasterise square at the preset DPI (fine 196 / superfine 391, default superfine), again clipped to 300 PPI. The (square) effective DPI is embedded in the output TIFF/PDF so the page is correctly proportioned, and the 300-PPI ceiling keeps per-page buffers at ~8 MP for letter-size regardless of source resolution.
(PDF embedded-image rectangles + a variance heuristic for full-page wrapped rasters). For colour sources a chroma gate also fires: a candidate block has to carry real colour to be classified as photo, which keeps grayscale form-table cells, message bodies, and printed text rows out of the halftone path (their variance alone is indistinguishable from a real photo). The chroma gate self-disables on grayscale sources. Document text and line art outside the photo region go to the adaptive binarizer; the photo region goes to a halftone schema. Never dither text.
6-page 391-DPI deck) and is only run when --recover-text is on. When it runs it provides the #808080 polarity recolour in two scopes:
recognised word is bucketed by the #808080 rule: median field luminance below 128 → glyphs marked WHITE; at or above 128 → glyphs marked BLACK. (--ocr-text off keeps the OCR pass but skips this scope.)
--recover-text on). Same #808080 rule, scopedto the photo region. Each word's ORIGINAL glyph pixels are segmented from the colour image and marked BLACK or WHITE per its sign's median field — preserving real letterforms, never retypesetting.
Without OCR (the default) the chroma-aware photo segmenter keeps form/ body/footer text out of the halftone path and the adaptive binarizer's contrast threshold pulls dark text to solid black against any near-white field — which is correct for the overwhelmingly common case. OCR is the escape hatch for white-on-coloured headers, signage inside photos, and captions baked into screenshots.
sits on any saturated-colour fill — a highlight chip behind a slide label, a tinted callout box, a colored table cell, a status badge in a dashboard, a filled banner, a colored form field — the bright fill collapses to a mid-tone in grayscale and the contrast binarizer flips polarity, painting the field solid black and knocking the text out as a mangled crosshatch. The preserve-text pass runs ahead of the binarizer: small (< 4% of page), high-chroma, low-variance regions that carry dark text strokes get lifted to white in the gray image so the dark text reads as crisp black-on-white through the standard binarize path. The "this is a highlight" colour cue is sacrificed — on 1-bit fax it was going to die anyway — and the text survives. Large colour panels and photo content are protected by the area cap and the ~photo mask; the rescue never touches the photo region. Disable with --no-preserve-text if you'd rather keep the fill as a halftoned tone band.
deskew (the photo mask and text masks ride the deskew rotation).
correction; --dither auto picks the schema).
document text stays crisp solid black.
forced to 0, WHITE pixels forced to 255. Because text rides this layer, the halftone screen can never disturb glyphs.
heavy-black pages.
(--format tiff) — losslessly, via img2pdf, never re-encoded. Pass --transmission-safe to also clamp the scanline to 1728 px.
confidences and polarities), total pages, and any warnings — so the result is inspectable before someone faxes something unreadable.
The mindset is optimize the document for the channel, not "make it look like a fax": keep text vector-crisp, render photos so they stay recognizable, and spend bits where they buy legibility. The halftone/binarization schema is the control surface for that — pick it deliberately.
A continuous-tone photo can't exist in 1-bit fax; it must be simulated with patterns of black dots. The schema chosen is the single biggest lever on how a photo reads after transmission, and each sits at a different point on the fidelity ↔ transition-density curve (transitions = compression cost = line fragility). The skill ships six front-line schemas spanning the design space (full theory in references/fax-optimization.md):
clustered — clustered-dot AM screening (newsprint). Dots grow inclusters → long runs → best G4 compression and most robust over a noisy line; lowest apparent resolution. The default for --fax-heavy.
green-noise — hybrid AM–FM screening. Mid-size dot clusters giveblue-noise detail with clustered-dot run-length and robustness; tune with --green-noise-coarseness (~2 detail … 8 robust). The auto picker's balanced fallback for moderate photo content; the picker chooses other members of the detail family — floyd/jarvis/edd/atkinson/clustered — when the page's content (mean/std luma, edge density, bimodality, fine texture) signals a clearer match.
blue-noise — void-and-cluster FM screening. Isotropic, organic stipplewith no directional "worms"; excellent perceived detail, middling compression.
atkinson — Atkinson error diffusion. Clean whites and crisp thinfeatures; good detail, looser compression than screening.
floyd — Floyd–Steinberg error diffusion. The classic; maximumdetail, and its dispersed fine-grain speckle is the richest signal to carry over the channel — more bytes shipped, more fidelity preserved.
line (aliases woodcut/engraving) — horizontal line screen. Tonebecomes horizontal stripes that thicken with darkness; because the strokes run along the scanline it produces almost entirely long runs → G4 size and robustness on par with clustered, while reading as a crisp, high-contrast engraving rather than mud. Great when a line is bad and the photo just has to stay recognizable.
(Also selectable: ordered Bayer; edd edge-enhancing error diffusion for text over a photographic background; jarvis/stucki/sierra heavier kernels; and none = hard threshold for pure text / line art / barcodes.) Because the pipeline runs at square pixels, the screens are isotropic by construction — dots stay round on paper without anisotropic correction.
Generalised AM screen — --dither screen --dot-shape SHAPE. clustered is one specific spot function; the screen family exposes the same machinery with selectable dot shape: round (default; byte-identical to clustered), square (crisp blocky), diamond (the classic newspaper-photo look), or ellipse (smoother midtone joins, chained vertically). --screen-angle θ rotates the dot lattice; off-axis angles trade some G4 compression (shorter horizontal runs) for the angled-screen aesthetic, and the pipeline logs an off_axis_screen:... warning for |θ| > 15°.
Crosshatch — --dither crosshatch. Layered angled-line screens lit by tone bands, reading as a pen-and-ink etching. Default --hatch-angles "0,90" keeps transition density close to line (compression is better than clustered on most pages because each scanline still ends up mostly horizontal runs). Three or four angles read as a denser engraving but multiply the transition count along every scanline — the pipeline warns with crosshatch_dense:....
Mezzotint — --dither mezzotint (expressive). Random-threshold grain — a velvety, unstructured stippling. Unlike blue-noise FM, mezzotint enforces no inter-dot spacing, so it has maximum 1-bit transition density per row: poor G4 compression and high line-noise sensitivity. It's not eligible for auto and trips an expressive_screen:mezzotint warning if you opt in. Use it when the artistic surface matters more than channel cost.
Algorithms can rank compression objectively, but only a human eye can judge "does this read?" So don't just pick silently — generate a side-by-side contact sheet with --sample N --panels K and let the user spend their eye tokens on the real, encoded output:
bashpython3 -m pdf_fax_optimizer.optimize_pdf INPUT.pdf -o OUTPUT.pdf \ --fax-resolution fine --sample 1 --panels 6 --report OUTPUT.report.json
--sample N --panels K renders that page through K side-by-side panels into one labeled PNG (OUTPUT.sample_pN.png), each panel annotated with its actual G4 size and estimated transmission time, with the auto-pick highlighted and a 3-line settings header at the top documenting the exact options used. --panels accepts 1, 2, 4 (default), 6, 8, 12, or 20 (max); each higher count is a strict superset of the smaller ones, so it's a clean detail dial — from "preview the optimal" (2 panels) to "compare a representative subset" (6 panels) to "every halftone screen in the registry" (20 panels).
The skill therefore does both jobs the user asked for: (a) it suggests the optimal method from the page's content (photo fraction, fax-heavy, line condition), and (b) it lets the user choose the optimal by eye from the contact sheet. Offer this whenever a fax has meaningful photo content — then re-run with the chosen --dither for the final file.
A fax is 1-bit and the receiver re-thresholds on luminance. The pipeline therefore enforces a single, brutally simple legibility contract for every word on the page: pick BLACK or WHITE by median field luminance vs #808080 (luma 128) — darker than 128 → text becomes WHITE; at or above 128 → text becomes BLACK. The recoloured glyphs ride a text layer composited ABOVE the halftoned image layer, so the halftone screen can never disturb them.
OCR (rapidocr-onnxruntime) is the word locator, not a typesetter. For every recognised word the pipeline segments its ORIGINAL glyph pixels from the colour image (using the OCR box's border ring as the field reference, which is robust where a blind 2-means split would invert) and stamps those exact pixels into the BLACK or WHITE text mask — preserving the real letterforms, spacing, and tilt. It never pastes a synthetic font.
The same function runs in two scopes — but the OCR engine itself only runs when --recover-text is on (it is the slow step; in default mode the chroma-aware photo segmenter and the adaptive binarizer handle text routing without it):
--recover-text (default off) is the master switch — itenables the OCR pass. When on it covers text INSIDE the image regions — signage, captions baked into a photo. Polarity is decided per sign (words grouped by proximity, one field tone for the whole group), so co- located words on the same plate get one consistent treatment. This is what brings back the cream "VILLA DEL SOL" lettering on the cyan/gold/ tan covers even when grayscale flattens it.
--ocr-text (default auto) is a sub-switch — when recover-textis on, this controls whether the same OCR pass also recolours text OUTSIDE the image regions (the page's header/footer/form text on coloured bars). Set it off to keep the within-image recolour while leaving doc text to the binarizer.
bashpython3 -m pdf_fax_optimizer.optimize_pdf cover.pdf -o cover.fax.pdf \ --sample 1 # 4-panel sheet of the recolor, side-by-side
rapidocr-onnxruntime (pip installrapidocr-onnxruntime — self-contained, no system binary). Without it the skill still works: document text falls back to the binarizer's default black-on-white, and the within-image recover pass becomes a no-op.
report (recover_text.words and ocr_text.words) with its confidence, polarity decision, measured field luminance, and WCAG contrast. Surface these to the user to verify. Low-confidence words (< --ocr-conf, default 0.5) are dropped.
--recover-text off disables only the within-image pass; document-textpolarity stays on (it's nearly always desirable).
--sample N [--panels K])--sample N writes a labelled contact sheet so the user can confirm the output is legible before sending. The default 4-panel layout is:
the 1-bit channel has to approximate.
halftone). The "naive fax" baseline the optimizations improve on.
recommended dither. The green border + >> RECOMMENDED << badge marks the skill's suggestion for this page.
Use --panels K to ask for 1, 2, 6, 8, 12, or 20 (max) panels — every higher count is a strict superset of the smaller ones:
--panels 1 — just the optimized output (single-panel preview)--panels 2 — original + recommended (minimal "before / after")--panels 6 — adds two representative halftones for a quick side-by-side--panels 20 (or max) — every screen in the SCREENS registry on YOURpage (the full catalogue, same layout as docs/readme/halftone_grid.png)
bashpython3 -m pdf_fax_optimizer.optimize_pdf cover.pdf -o cover.fax.pdf --sample 1 # writes cover.fax.sample_p1.png python3 -m pdf_fax_optimizer.optimize_pdf cover.pdf -o cover.fax.pdf --sample 3 --panels max # 20-panel "every halftone screen" sheet for page 3 of a multi-page deck python3 -m pdf_fax_optimizer.optimize_pdf cover.pdf -o cover.fax.pdf --sample 1 \ --sample-include orig,gray,clustered,floyd,line # custom recipe
Each fax panel is rendered with the user's actual options (preserve_text, ocr_text, recover_text, etc.) so the sheet is a faithful preview of the real fax. A 3-line settings header at the top documents which options produced the sheet — saved PNGs stay self-describing weeks later. Pass --no-sample-header to omit it.
Always run the report and skim the warnings. Watch for: barcodes / QR codes (verify they survive bilevel — they want a clean threshold, not dither), signatures / handwriting (thin low-contrast strokes — candidates for thickening), already-bilevel input (detect and skip reprocessing), and large black / inverted areas (slow and error-prone to transmit).
Instead of flags, pass --config config.json. Schema and an annotated example: references/config-schema.md. Flags override config values.
Always finish by telling the user the output path(s), the total pages + estimated transmission time, and any warnings the report flagged. Legibility is the acceptance test:
(--sample N) showing original / grayscale / default fax / recommended side by side. This is the fastest way to confirm aspect ratio, that the halftone is on image areas only, and that the recommended pick reads.
--sample N --panels 1.
halftone schemes, ask for more panels: --sample N --panels 6 for a curated comparison, --panels 12 or --panels max (=20) for a full catalogue. The skill highlights its recommended/optimal pick on every sheet, but the human makes the final call by eye.
before/after (--recover-text-preview N) so the user can confirm the within-image recolor was helpful.
If anything is borderline — small text, faint signatures, muddy photos — recommend the knob that recovers it (--thicken, a higher --fax-resolution, or a different --dither) rather than shipping an unreadable fax.
The skill can also transmit the optimized file through a cloud fax API — no machine, modem, or phone line, just an API key and the recipient number in E.164 (e.g. +14155551234). Built-in providers: mfax (mFax/Documo, the default), phaxio (Phaxio/Sinch), and generic (any upload API — Telnyx, SRFax, etc.). Full details and per-provider request shapes: references/sending.md.
Optimize and send in one step (always pass keys via env, and --dry-run first to confirm the request before transmitting):
bashexport MFAX_API_KEY=sk_live_xxx python3 -m pdf_fax_optimizer.optimize_pdf INPUT.pdf -o OUTPUT.fax.pdf \ --transmission-safe \ --send mfax --to +14155551234 --dry-run # drop --dry-run to transmit
Pass --transmission-safe whenever the file will hit a real Group-3 line — it clamps the rendered scanline to 1728 px so a real fax machine can transmit it.
Or send an already-optimized file directly:
bashpython3 -m pdf_fax_optimizer.send_fax OUTPUT.fax.pdf --provider mfax --to +14155551234
Rules: never put an API key on the command line (use the provider's env var); confirm the recipient number with the user before a real send; show the user the --dry-run request and the legibility check (preview/report) before transmitting; and remember a submit response means queued, not delivered — point the user to the provider's status webhook/endpoint to confirm receipt.
references/fax-optimization.md — the fax constraint model, resolutions,dithering theory, MRC segmentation, legibility defense, transmission economics. Read before running.
references/config-schema.md — full JSON config schema + annotated example.references/sending.md — transmit via a cloud fax API (mFax, Phaxio,generic): endpoints, auth, env vars, and one-step optimize-and-send.
Other measured skills in the registry, with their headline benchmark lift.