プリントサーバー

bdf printd はプリントサーバーです。HTTP でジョブを受け、1 つのファイルに保存し、順に IPP のプリンタ(と 9100・LPD のプリンタ)へ送り、プリンタが刷り終えるまで見届け、届かないプリンタには送り直します。業務システムは文書を渡したら先へ進み、待つのはサーバーです。プリンタとは bdf print と同じく、OS のドライバを使わずに IPP で話します。

bdf printd -config printd.yaml                           # 前面で動く。Ctrl-C か SIGTERM で止まる
bdf print -server http://127.0.0.1:8640 -queue office invoice.bdf     # 投入して、ジョブの id を出して帰る
bdf print -server http://127.0.0.1:8640 -queue office -wait 10m report.xlsx   # 刷り終えるまで見届ける
bdf jobs -server http://127.0.0.1:8640                    # 終わっていないジョブ
bdf printers -server http://127.0.0.1:8640                # キューとプリンタ

$BDF_SERVER と $BDF_TOKEN が -server とトークンの代わりになります。ブラウザで http://127.0.0.1:8640/ を開くと、キュー・プリンタ・ジョブの状態が変わるたびに見えます。

すること

設定

listen: 127.0.0.1:8640              # 既定
spool: /var/lib/bdf/spool.db        # 既定はこのファイルと同じディレクトリの spool.db
user: bdf                           # プリンタに見せる requesting-user-name
tls: { cert: /etc/bdf/tls.crt, key: /etc/bdf/tls.key }   # loopback 以外では必須
tokens:
  - { name: erp, file: /etc/bdf/erp.token, queues: [office, warehouse], roles: [submit], act-as: true }
  - { name: ops, env: BDF_OPS_TOKEN, roles: [submit, manage, admin] }
printers:
  mfp1: { url: "ipps://mfp1.example.local/ipp/print", fingerprint: "sha256:…" }
  mfp2: { url: "ipp://192.0.2.20/ipp/print", username: print, password-env: MFP2_PASSWORD }
  old-laser: { url: "socket://192.0.2.30:9100" }                     # IPP の無いプリンタ: 生のポートに PostScript
  plotter: { url: "lpd://192.0.2.31/lp1", format: pdf, pjl: true }   # LPD で PDF、PJL の見出し付き
  labels: { url: "socket://192.0.2.40:9100", profile: zd421-203 }    # ZPL のラベル
  register: { url: "serial:///dev/ttyUSB0?baud=19200", format: escpos }   # ESC/POS のレシート
  zebra-usb: { url: "ipp://localhost:631/printers/zebra", profile: zd421-300 }   # CUPS の raw キュー
queues:
  office:
    printers: [mfp1, mfp2]
    policy: failover                # か any
    defaults: { sides: two-sided-long-edge, print-color-mode: monochrome }
    format: auto                    # auto(PDF、PostScript、PWG Raster、Apple Raster、PCLm の順に受けるもの)、pdf、ps、pwg-raster、urf、pclm
    raster-if-lossy: true
    max-inflight: 2
  warehouse: { printers: [mfp2] }
retention: { documents: 7d, records: 400d }
limits: { max-document: 256MiB, max-jobs: 10000, max-bytes: 10GiB, max-impressions: 10000,
          renders: 0, retry-for: 24h, uploads: 8, events: 64 }
fonts: { dirs: [/usr/share/fonts/bdf], no-system: false }
temp-dir: /var/tmp/bdf

bdf printd -config printd.yaml -check は設定を読み、各プリンタにできることを聞いて表にします。スプールは開きません(答えないプリンタがあれば終了コード 1)。

ipp: はキューを IPP プリンタとして見せ(後述)、upstreams: と proxy: true のプリンタは NAT を越えてジョブを取りに行かせます(後述)。

誰が何をできるか

API

JSON で /api/v1 の下。エラーは {"error": {"code": "…", "message": "…"}} と HTTP の状態です: 400 invalid か bad-document、401 unauthorized、403 forbidden か wrong-password、404 not-found か no-queue、409 not-possible(ジョブの状態ではできない)、413 too-large、415 unknown-format、503 not-accepting か busy、507 full。

メソッドと道
POST /queues/{q}/jobs 投入。multipart/form-data で document(ファイル)と、順不同で ticket と options(JSON)、name、password、priority、hold-until、hold-until-time。201 とジョブ
GET /jobs?queue=&user=&state=&after=&limit= 終わっていないジョブ。state=all で終わったものも、または状態で絞る。after と next で id 順に続きを取る
GET /jobs/{id} ジョブ
POST /jobs/{id}/cancel・hold・release・retry・password・move 取り消し(プリンタでも)、保留、解除({"password"} も)、送り直し、パスワードを渡す、移す({"queue"})
PATCH /jobs/{id} {"priority": n}
GET /queues、GET /queues/{q} キューと、状態ごとのジョブの数
POST /queues/{q}/pause・resume・reject・accept 送るのを止める・再開する・受け付けを止める・再開する(admin)
GET /printers、GET /printers/{p} プリンタと状態、1 台のできること
GET /discover?timeout= DNS-SD でネットワークの IPP プリンタを探す(admin)
GET /accounting?from=&to=&format=jsonl|csv 終わったジョブ: 誰が何をどこで何枚刷ったか(manage)
GET /backup spool.db の一貫した写し(admin)
GET /events?job= Server-Sent Events。job、queue、printer とそれぞれの JSON
curl -F document=@invoice.bdf -F 'ticket={"sides":"two-sided-long-edge","copies":2}' \
     -H "Authorization: Bearer $TOKEN" https://print.example.local:8640/api/v1/queues/office/jobs

チケットには、ジョブが自分について言うことも書けます。サーバーはジョブと一緒に持ち、IPP で答え、プリンタにも送ります。job-account-id と job-accounting-user-id(課金先の部門やプロジェクトと、その利用者。受けると言うプリンタに頼みます)、document-metadata(文書を説明する key=value の文字列。キーは Dublin Core の title、creator、identifier、subject、description、date など、または x- で始まる独自のもの。プリンタはジョブと一緒に持ちます)、bdf-attributes(x- で始まる独自のジョブ属性。{"x-cost-center": "cc-42", "x-tags": ["a", "b"]} のように書き、そのまま送ります。知らないプリンタはそう答え、それ抜きで刷ります。strict なら失敗します)。bdf print では -account、-accounting-user、-metadata key=value、-attribute x-name=value です。

/api/v1 の外に、/healthz(動いている間は 200)、/metrics(Prometheus のテキスト。キューと状態ごとのジョブ、終わったジョブ、枚数、プリンタの状態、送ったバイト、送り直し、スプールの大きさ)、/(状態のページ。読むだけで、トークンはページで渡せます)があります。

Go からは print/server/client が API のクライアントです(Submit、Job、Jobs、Cancel など、Wait、Events、Accounting)。bdf のほかの部分に依りません。プログラムにサーバーを組み込むこともできます。print/server が設定のスプールを開き、http.Handler を返します(サンプル)。

暗号化した文書

暗号化した .bdf はスプールでも暗号化のままです。ジョブと一緒に渡したパスワード(bdf print は $BDF_PASSWORD を、先に確かめてから送る)はメモリにだけ持ちます。再起動の後のジョブは、渡し直す(bdf jobs password <id>)まで待ちます(pending-held、job-password-wait)。パスワードの要る PDF や Office 文書はそのパスワードで変換し、変換した .bdf も同じパスワードで暗号化して置きます。bdf print は、TLS の無い接続で別の機械へパスワードを送りません(-allow-plaintext のときを除く)。

IPP プリンタとして見せる

設定に ipp: があると、各キューは IPP Everywhere のプリンタにもなり、どの OS もドライバなしで追加できます。Windows(Windows Ready Print と同じ IPP クラスドライバ)、macOS と iOS(AirPrint)、Android(既定の印刷サービスの Mopria)、ChromeOS と Linux(CUPS)。送ってくるのは PDF(先頭に出すので、ベクタのまま送られる)、Apple Raster か PWG Raster、JPEG か PNG で、サーバーは受けたものを投入と同じく .bdf にしてキューのプリンタで刷ります。ラスタのページは、描かれた用紙の大きさの絵になります。

ipp:
  listen: ":8631"              # 既定。ipp と ipps を同じポートで
  host: print.example.com      # プリンタの URI と証明書の名前。既定は機械の名前の .local
  auth: none                   # none(LAN のプリンタと同じ)か basic(トークンの名前と秘密)。トークンがあれば basic
  require-tls: false           # ipp を断る(HTTP 426)。basic のときはいつも
  tls: { cert: /etc/bdf/ipp.crt, key: /etc/bdf/ipp.key }   # 無ければ自己署名を作る(スプールの隣の ipp-tls.pem)
  queues: [office, warehouse]  # 見せるキュー。既定は全部
  default: office              # /ipp/print にも
  location: "2F"               # printer-location。キューの location が優先
  organization: Example Inc.
  dns-sd: true                 # LAN に広告する(mDNS)。既定
queues:
  office: { printers: [mfp1], info: "Office printer", location: "2F north" }

プリンタの追加:

OS
Windows 10・11 設定 → Bluetooth とデバイス → プリンターとスキャナー → デバイスの追加。キューが一覧に出ます(mDNS)。別のサブネットは「IP アドレスまたはホスト名を使ってプリンターを追加する」→ IPP デバイス → https://host:8631/ipp/print/office(か http://)。IPP クラスドライバが使われ、ドライバは入りません
macOS システム設定 → プリンタとスキャナ → プリンタを追加。一覧に出ます(AirPrint)。別のサブネットは IP のタブで、プロトコル IPP、アドレス host:8631、キュー ipp/print/office。シェルからは lpadmin -p office -E -v ipps://host:8631/ipp/print/office -m everywhere
iPhone・iPad 同じサブネットなら印刷のときに一覧に出ます(AirPrint)。別のサブネットは上のプロファイル
Android 既定の印刷サービス(Mopria)の一覧に出ます。別のサブネットはサービスの設定でアドレスから
ChromeOS 設定 → プリンタ → プリンタを追加。一覧に出るか、アドレスとプロトコル IPP / IPPS、パス ipp/print/office で
Linux lpadmin -p office -E -v ipps://host:8631/ipp/print/office -m everywhere、またはデスクトップのプリンタ設定(一覧に出ます)

NAT の向こうの拠点

本社のサーバーから届かない拠点(NAT の内側で VPN が無い)は、拠点に自前の bdf printd を置き、本社のキューのジョブを取りに行って拠点のプリンタで刷らせます。つなぐのは拠点からで、本社からは拠点へつなぎません。両者が話すのは IPP の共有インフラ拡張(PWG 5100.18)なので、同じ規格の他の代理も取りに来られます。

# 本社: プリンタが拠点の代理であるキュー
printers:
  osaka: { proxy: true }
queues:
  osaka: { printers: [osaka] }
ipp: { listen: ":8631", host: print.example.com }
tokens:
  - { name: osaka-proxy, file: /etc/bdf/osaka.token, roles: [proxy], queues: [osaka] }
# 拠点
printers:
  mfp: { url: "ipp://192.0.2.10/ipp/print" }
queues:
  local: { printers: [mfp] }
upstreams:
  hq:
    url: ipps://print.example.com:8631/ipp/print/osaka
    queue: local
    username: osaka-proxy
    password-file: /etc/bdf/osaka.token
    poll: 10s              # 既定
    # fingerprint: "sha256:…"   本社の証明書を固定する。無ければ最初に見たものを覚える

ラベル・レシート・連帳

ラベル・レシート・連帳のプリンタのキューも、ほかのキューと同じもの(穴埋めした帳票、PDF、画像 …)を受け、そのプリンタの言語で刷ります(細部は bdf print)。

言語 プリンタ 送るもの
ZPL Zebra と ZPL II の互換機 ラベルごとに 1 つのフォーマット。ドットは ^GFA(ZPL の ASCII 圧縮)、QR コードとバーコードは ^BQ・^BC・^BE …、部数は ^PQ
SBPL SATO ラベルごとに STX と ETX で囲んだ <A> … <Z>。<G>H のグラフィック、<2D30>・<B>・<BG> の記号、<Q> の部数
TPCL 東芝テック {D の大きさ、ニブルモードの {SG;、{XB・{RB の記号(Code 128 は除く)、{XS;I の発行
ESC/POS Epson TM、レシートプリンタ 描いた帯から順に GS v 0、GS V のカット、ESC p のドロワ
ESC/P Epson VP、ドットインパクト 24 ピンのビットイメージ(ESC *、180 dpi)、ESC C の用紙長、ESC N のミシン目スキップ、改ページ

キューの defaults には、こうしたプリンタへの指示もチケットの名前で書けます: print-darkness、print-speed、media-tracking(IPP の名前)と、bdf-label-gap、bdf-cut、bdf-cash-drawer、bdf-perforation-skip、bdf-no-printer-barcodes、bdf-print-mode(raster か text。テキストのテンプレートの帳票の文字をプリンタのフォントで刷る。ESC/P はそうした帳票を既定でテキストモードで刷る)。部数はプリンタが作り(^PQ、<Q>、{XS)、レシートと連帳は繰り返して送ります。

命令は各社のプログラミングリファレンス(Zebra の ZPL II、SATO の CL4NX Plus と CT4-LX、東芝テックの B-EX4・B-FV4・BX400 の外部インタフェース仕様、Epson の ESC/POS と ESC/P)に従っています。確かめたのは自前の読み手でドットに戻すところまでで、実機では確かめていません。大量に刷る前に 1 枚試し、記号がおかしければ bdf-no-printer-barcodes: true でドットとして描かせてください(203 dpi でも読めます)。SBPL は SATO のステータスのプロトコルが求めるとおり STX と ETX で囲みます。USB のプリンタには、Windows はドライバのキュー(winspool:///名前、RAW で渡す)、macOS は CUPS(ipp://localhost:631/printers/<キュー>)、Linux はどちらかか usb:///dev/usb/lp0 で届きます。

常駐させる

bdf printd は前面で動き、ログを標準エラーに書き(JSON。人が読むなら -log text)、SIGINT か SIGTERM(Windows は Ctrl-C か Ctrl-Break)で止まります。止まるときは受け付けをやめ、送っている文書を 30 秒まで待ってからスプールを閉じます。途中で打ち切ったものは次の起動で直します。サービスとしての登録は持たず、常駐は OS のサービス管理に任せます。

Linux(systemd): /etc/systemd/system/bdf-printd.service を置いて systemctl enable --now bdf-printd。

[Unit]
Description=bdf print server
After=network-online.target
Wants=network-online.target

[Service]
# 専用の利用者(useradd --system bdf)。トークンのファイルを読めること
User=bdf
ExecStart=/usr/local/bin/bdf printd -config /etc/bdf/printd.yaml
Restart=on-failure
# printd.yaml の spool: /var/lib/bdf/spool.db
StateDirectory=bdf
TimeoutStopSec=45

[Install]
WantedBy=multi-user.target

macOS(launchd): /Library/LaunchDaemons/io.github.shibukawa.bdf-printd.plist を置いて sudo launchctl bootstrap system /Library/LaunchDaemons/io.github.shibukawa.bdf-printd.plist。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>io.github.shibukawa.bdf-printd</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/bdf</string><string>printd</string>
    <string>-config</string><string>/usr/local/etc/bdf/printd.yaml</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>StandardErrorPath</key><string>/usr/local/var/log/bdf-printd.log</string>
</dict>
</plist>

Windows: NSSM や WinSW のようなサービスの包みで動かします。

nssm install bdf-printd "C:\Program Files\bdf\bdf.exe" printd -config "C:\ProgramData\bdf\printd.yaml"
nssm set bdf-printd AppStopMethodConsole 30000
nssm set bdf-printd AppStderr "C:\ProgramData\bdf\printd.log"
nssm start bdf-printd
<!-- bdf-printd.xml。bdf-printd.exe に改名した WinSW の隣に置き、bdf-printd.exe install、start -->
<service>
  <id>bdf-printd</id>
  <name>bdf print server</name>
  <executable>C:\Program Files\bdf\bdf.exe</executable>
  <arguments>printd -config "C:\ProgramData\bdf\printd.yaml"</arguments>
  <stoptimeout>30 sec</stoptimeout>
  <onfailure action="restart" delay="10 sec"/>
  <log mode="roll"/>
</service>

包みを使わないなら、タスク スケジューラで起動時に動かせます: schtasks /create /tn bdf-printd /sc onstart /ru SYSTEM /tr "\"C:\Program Files\bdf\bdf.exe\" printd -config C:\ProgramData\bdf\printd.yaml"(落ちたときに起動し直しはしません)。

スプールの手入れ