Introduction
November is finally here. Thanks to everyone’s support, it’s been a full year since I started developing ACR (Across Report), a report-rendering engine. Over that year, ACR has also grown to support various features, including Visual Studio Code and npm integration. This time, I’d like to talk about its connection with the fax-sending platform (acr-fax).
Fax is not legacy here — it's infrastructure
I've spent about 25 years building business applications for the retail and distribution industry in Japan. For a long time, ordering between retailers and suppliers ran, in no small part, on decades-old industry-standard protocols: the "Zengin procedure" and the so-called "JCA procedure" (leased-line EDI standards from Japan's banking/retail EDI history). Both were retired at the end of 2023 — Zengin TCP/IP, a separate and still-current standard, continues to be used today, but the older Zengin procedure and JCA procedure are now gone.
Not every supplier can support these protocols. For the ones that can't, a common workaround has been to take the EDI order data, render it as a printable form, and send it over fax — a system commonly called FAXEOS. This turned out to be remarkably practical:
- Head office and stores keep ordering normally through handheld terminals, with zero awareness of what protocol the supplier actually speaks
- Suppliers only need to prove they can receive a fax to be onboarded — no leased line, no complex connection setup
- It doubles as a failover path when the EDI system itself goes down
When WebEDI showed up later, FAXEOS didn't disappear. Some suppliers still can't do EDI at all, and some who adopted WebEDI still ask to keep receiving orders by fax in parallel. Japan also has a distribution custom called "chōai torihiki" (帳合取引), where small-value transactions are often routed through a wholesaler/trading-company intermediary — which means the protocol question is rarely a clean one-to-one relationship between a retailer and a supplier. Whatever EDI standard eventually wins (today that's largely "Ryutsu BMS" in Japanese retail), there will always be a tail of suppliers it can't reach. Fax fills that gap, and it has for a long time.
This post is about rebuilding that FAXEOS-style capability myself, on a modern stack, and getting it confirmed working across multiple machines. Integration with my own report-rendering engine (ACR) isn't done yet — this post is strictly about the fax-sending foundation itself.
The stack
Avalonia UI (.NET, acr_fax_client) │ spawns as external process, reads JSON output ▼ acr_fax_server (Rust CLI) │ └─ talks hfaxd protocol directly (port 4559, JSUBM, etc.) ▼ HylaFAX (hfaxd / faxq / faxgetty) │ ▼ USB modem → analog phone line Why talk to hfaxd directly instead of shelling out to sendfax
The obvious approach would be wrapping HylaFAX's own CLI tools (sendfax, faxstat) as subprocesses. I didn't go that route, because an earlier C# prototype had already proven out something better: connecting directly to hfaxd's own protocol on port 4559 and submitting jobs with JSUBM. Porting that protocol implementation to Rust turned out to be less work than building a CLI-wrapping layer, and it gives much finer control over job parameters.
The tradeoff is that you inherit the protocol's own sharp edges. The one that bit me early: if you don't explicitly set a job expiry, hfaxd treats it as expiring in 1970 and rejects the job outright. You have to set it explicitly:
JPARM LASTTIME 000300 Splitting the conversion path by input type
| Input | Covered by HylaFAX's standard typerules? | Approach |
|---|---|---|
Yes (auto-converted via pdf2fax + Ghostscript) | Pass through as-is | |
| PNG | No | Needs a conversion module — shell out to Ghostscript (gs -sDEVICE=tiffg4) |
Rust's TIFF ecosystem (e.g. image-tiff) doesn't support writing CCITT Group 3/4 encoding, and implementing that myself seemed like high risk for low reward. Reusing HylaFAX's own battle-tested conversion pipeline was the pragmatic call.
Keeping the core rendering engine untouched
My report engine, ACR, treats its RenderCommand as the single source of truth for everything it renders — PDF, PNG, whatever comes next. I didn't want fax support to leak into that. All of the fax-specific logic lives entirely in acr_fax_server, as an additional layer on top, not a change to the rendering core. That keeps the door open for other output targets down the line (ESC/POS receipt printers, label printers, etc.) without them stepping on each other.
Where it stands
Multi-machine send has been confirmed working end-to-end — queued through acr_fax_server, delivered through HylaFAX, confirmed received on the other end (faxstat -s showing status D for Done).
Integration with ACR's report generation isn't wired in yet. Right now, sending requires PDF/PNG/Office files directly, or a manually-prepared template+data pair.
From single-vendor to multi-vendor
The old FAXEOS setup was Windows-only, tied to a specific fax software and modem combination — hard to swap out once committed to it. This time, the sending client (built on Avalonia) and the fax server (Linux/HylaFAX) are split apart, talking over the open hfaxd protocol. That means the modem/server side can be swapped for anything HylaFAX supports, without being locked into one vendor's stack — a side benefit of going with a direct protocol implementation instead of a vendor-specific wrapper. The client itself is cross-platform, too, and has actually been tested running on Windows, Linux, and macOS.
What's next
The main direction going forward is supporting Ryutsu BMS (Japan's current distribution EDI standard) as a complementary path — specifically, converting EDI to fax for trading partners further down the chain who can't speak EDI themselves. No matter which EDI standard becomes dominant, there's always a tail of partners it can't reach, and that's where this keeps being useful.
Before that, a few practical follow-ups:
- Packaging/distributing prebuilt binaries
- A reproducible guide for installing HylaFAX on Ubuntu
- Running the HylaFAX side under WSL, and what breaks (USB modem passthrough, mainly)