Webアプリからローカルプリンタへ直接出力:QZ Trayでのサイレント印刷実装と警告抑止Tips
Webアプリケーションからローカルのプリンタへ、ラベルや帳票を直接出力したいケースはよくあります。しかし、通常のブラウザの仕様では印刷ダイアログが毎回表示されてしまい、ネイティブアプリのようなシームレスな印刷体験(UX)を提供するのは困難です。
最近、この課題を解決するために「QZ Tray」というツールを利用しました。非常に便利で強力なツールでしたが、ブラウザのボタンを押した瞬間に直接印刷させる「サイレント自動印刷」を実現するまでに、セキュリティ警告の抑止で少し手間取ったので、備忘録として実装方法をまとめます。
課題(Situation / Task)
QZ Trayは、ローカルPCに常駐アプリをインストールし、Webアプリ側からJavaScriptのライブラリを利用することで、ブラウザの制限を越えてローカルプリンタを直接制御できるツールです。
しかし、ただ出力を組み込んだだけでは、印刷リクエストを行うたびに「Untrusted website」というセキュリティ警告ポップアップが出現してしまいます。これは「悪意のあるウェブサイトが勝手にユーザーのプリンタで印刷するのを防ぐ」ためのQZ Trayの強固なセキュリティ機構によるものです。
実際の運用において、現場のスタッフが毎回この警告を目視して「許可」ボタンを押すのは現実的ではなく、これではQZ Trayを導入した意味が半減してしまいます。サイレント印刷を実現するためには、このポップアップを抑止する必要がありました。
解決策(Action)
警告ポップアップを抑止するためには、「電子署名(Digital Signature)」の仕組みをシステムに組み込み、QZ Trayに「このWebサイトからのリクエストは信頼できる」と認識させる必要があります。
具体的には以下の手順で実装を行いました。
Step 1: バックエンドでの署名APIの作成(Laravelの例)
サーバー側に「秘密鍵(Private Key)」と「公開鍵証明書(Public Certificate)」を配置し、フロントエンドからの要求に応じてバックエンドで署名を行うAPIを構築します。
1. 鍵と証明書の生成・配置
サーバー環境(今回は AlmaLinux 8 / PHP 8.3 / Laravel 環境)にて、qz-private.pem (秘密鍵) と qz-cert.pem (公開鍵証明書) を生成し、配置します。
2. 署名用APIエンドポイントの作成
Laravelのルーティング(routes/api.php)にエンドポイントを用意し、コントローラーに以下の処理を実装します。QZ Trayから送られてくるハッシュ対象の文字列を受け取り、秘密鍵と openssl_sign (SHA512) を使って署名し、Base64エンコードして返却します。
// QzTrayController.php
public function sign(Request $request)
{
// QZ Trayから送信されるハッシュ対象の文字列
$message = $request->input('request');
// 配置した秘密鍵を読み込み
$privateKey = Storage::get('private/qz-private.pem');
// SHA512で署名を実行
openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA512);
// Base64エンコードし、プレーンテキストとしてレスポンスを返す
return response(base64_encode($signature), 200)
->header('Content-Type', 'text/plain');
}
Step 2: フロントエンドのセキュリティ設定 (JavaScript)
次に、印刷を実行する画面のJavaScript側で、QZ Trayの初期化処理(qz.websocket.connect() を呼ぶ前)にセキュリティ設定を追加します。
// 1. 公開鍵証明書の読み込み先を指定
qz.security.setCertificatePromise(function(resolve, reject) {
resolve("/path/to/qz-cert.pem");
});
// 2. 署名アルゴリズムの指定と、署名APIの呼び出し設定
qz.security.setSignatureAlgorithm("SHA512");
qz.security.setSignaturePromise(function(toSign) {
return function(resolve, reject) {
fetch('/api/qz/sign?request=' + encodeURIComponent(toSign), { cache: 'no-store' })
.then(response => {
if (!response.ok) throw new Error('Failed to sign the request');
return response.text();
})
.then(signatureText => resolve(signatureText))
.catch(err => reject(err));
};
});
Step 3: ローカルPCへの証明書(公開鍵)登録
上記の実装によって、QZ Trayの詳細メッセージで署名が「Valid(有効)」と認識されるようになります。しかし、私のケースではオープンソース版(無料版)のQZ Trayを使用しており、かつ「自己署名証明書」を利用していたため、依然として警告ポップアップが表示されました。
さらに、悪用防止の強力なロック機構により、「Remember this decision(この決定を記憶する)」のチェックボックスをONにすると「Allow(許可)」ボタンが押せなくなってしまいます。
これを解決するためには、ローカルPC側に直接証明書をインストールする作業が必要です。
- サーバー上にある公開鍵(
qz-cert.pem)をローカルPCにダウンロードします。 - タスクトレイのQZ Trayアイコンを右クリックし、「Advanced」 > 「Site Manager」を開きます。
- 画面内の「+」ボタンをクリックし、「Browse…」からダウンロードした
qz-cert.pemを読み込ませてインストールします。
結果(Result)
証明書をローカルPCに登録したあと、ブラウザをリロードして再度出力を実行すると、QZ Trayが「登録済みの信頼できる証明書からのリクエスト」と認識し、警告ポップアップが表示されることなく即座にサイレント印刷が実行されるようになりました。
プリンタ設定UIの構築と出力先の出し分け
サイレント印刷の仕組みができた後、QZ Trayの便利なAPI qz.printers.find() を活用して使い勝手をさらに向上させました。
このAPIは「現在そのPCにインストールされている全プリンタのリスト」を取得できます。これを利用し、ユーザーが手入力でプリンタ名を設定するのではなく、「プルダウンから選ぶだけ」のUIを構築しました。
選択したプリンタ情報はブラウザの localStorage に保存し、印刷リクエスト時に「ラベルは業務用ラベルプリンタへ、インボイス(帳票)は複合機へ」といった用途別の出力先切り替えもスムーズに実装できました。
運用についての補足
今回実施した「PCごとに証明書を登録する作業」は、無料版における標準的な手法であり、小規模なネットワークや限られた拠点内の数台のPCであれば、この運用で問題ない認識です。
もし「システムに詳しくない不特定多数の外部ユーザー」に機能を提供するようなケースでは、QZ Industries社からプレミアムライセンス(有料)を購入することで、最初から信頼されている正式な証明書を利用でき、クライアントPC側での証明書インストール作業を省略することもできるようです。
最終的な印刷体験はネイティブアプリ並みで非常に感動しましたが、電子署名のフロー確立とポップアップ抑制には手間取ったため、同じような要件を実装される方の参考になれば幸いです。
追記:なぜWebからローカルプリンタを直接制御できるのか?(Mixed Content制限とLocalhostの特例)
通常のWebアプリケーション開発において、フロントエンド(JavaScript)からユーザーのPCに繋がっているローカルプリンタやファイルシステムへ直接アクセスすることは、ブラウザの強力なセキュリティ機構(サンドボックス)によって禁止されています。
さらに、現代のWebサイトは原則「HTTPS(暗号化通信)」で配信されています。HTTPSのページから、暗号化されていないローカルのHTTPや標準のWebSocket(ws://)へ通信を行おうとすると、ブラウザは「混合コンテンツ(Mixed Content)」として通信を強制的にブロックします。
では、なぜQZ Trayはブラウザの制限を越えて、ローカルのプリンタへ直接データを流し込めるのでしょうか?その答えは、ブラウザが持つ 「Localhostの特例」 にあるようです。
W3Cが定めるWeb標準仕様「Secure Contexts(安全なコンテキスト)」において、宛先が自分自身のPCである localhost や 127.0.0.1 に対する通信は、外部ネットワークに露出しないため「潜在的に安全(Potentially Trustworthy)」であると定義され、セキュリティ制限の例外として扱われます。
QZ Trayのアーキテクチャは、この仕様を巧みに利用しています。
QZ Trayのアーキテクチャ公式ドキュメントによると、PCに常駐するQZ TrayアプリがバックグラウンドでローカルのWebSocketサーバー(wss://localhost)として待機し、Web上のJavaScriptからそのローカルサーバーへ直接トンネルを開通させているようです。
通信先が localhost であるためブラウザのMixed Content制限を正規のルートでクリアし、同時に今回実装した「バックエンドでの電子署名(Digital Signature)」によって悪意のあるサイトからの不正アクセスを弾く。この2段構えのアーキテクチャによって、Webアプリでありながらセキュリティと「ネイティブアプリのような印刷体験」を両立させているようです。素晴らしいソフトウェアを開発・提供されていることに感謝します。