帳票やレシートを紙に出す方法は 2 つあります。アプリの画面(HTML)を印刷するなら、WebView の window.print() で印刷ダイアログを出すのが手軽で、プリンターや部数はユーザーが選べます。プリンターの一覧を自前の画面に出したい、決まったプリンターへダイアログなしで送りたいときは、Rust の printers クレートを使います。JS にはプリンターの一覧を取る API が無いので、一覧は Rust から受け取ります。つまずきやすいのは、Windows ではファイルの中身が加工されずにプリンターへ届くことです。
前提条件
src-tauri でクレートを追加します。コードは printers 2.3 系向けです。
cd src-tauri
cargo add printers
Linux でビルドするには CUPS の開発用ファイル(Debian / Ubuntu は libcups2-dev、Fedora は cups-devel)が要ります。
自作コマンドの呼び出しに権限の追加は要りません。window.print() は macOS だけ Tauri のコマンドを経由してダイアログを開くので、core:default に含まれない core:webview:allow-print を足します。Windows・Linux では WebView が自分でダイアログを出すので、権限が無くても動きます。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:webview:allow-print"
]
}
| やりたいこと | 使うもの |
|---|---|
| 画面や帳票をダイアログから印刷する | window.print() |
| プリンターの一覧・既定のプリンターを出す | get_printers() |
| PDF などのファイルをダイアログなしで送る | print_file()(Windows は注意) |
| レシート・ラベルプリンターに命令を送る | print() にバイト列 |
1. フロントエンドから実装する (TypeScript)
画面を印刷する(window.print())
印刷したい内容を専用の要素に入れてから window.print() を呼びます。ダイアログで Windows の「Microsoft Print to PDF」のような PDF に出力するプリンターを選べば、紙を使わずに仕上がりを確かめられます。
// 渡した要素だけを印刷する(#print-area は画面では隠しておく)
export function printElement(content: Node): void {
const area = document.querySelector<HTMLElement>('#print-area');
if (!area) throw new Error('#print-area がありません');
area.replaceChildren(content);
window.print(); // macOS では core:webview:allow-print が必要
}
印刷時だけの見た目は CSS の @media print で決めます。実際に印刷されたかキャンセルされたかは、ページからは分かりません。
#print-area { display: none; }
@media print {
body > * { display: none !important; }
body > #print-area { display: block !important; }
@page { margin: 15mm; }
}
Rust で取った一覧から印刷先を選ぶ
一覧を <select> に入れ、前回選んだプリンター、無ければ既定のプリンターを選択しておきます。
import { invoke } from '@tauri-apps/api/core';
type PrinterInfo = {
name: string; // 表示用の名前
systemName: string; // 印刷先の指定に使う名前
isDefault: boolean;
state: 'ready' | 'printing' | 'paused' | 'offline' | 'unknown';
};
const KEY = 'printer';
const select = document.querySelector<HTMLSelectElement>('#printer')!;
select.addEventListener('change', () => localStorage.setItem(KEY, select.value));
export async function loadPrinters(): Promise<PrinterInfo[]> {
const list = await invoke<PrinterInfo[]>('list_printers');
select.replaceChildren(
...list.map((p) => new Option(`${p.name}${p.isDefault ? '(既定)' : ''}${p.state === 'offline' ? '(オフライン)' : ''}`, p.systemName)),
);
const saved = localStorage.getItem(KEY);
const pick = list.find((p) => p.systemName === saved) ?? list.find((p) => p.isDefault);
if (pick) select.value = pick.systemName;
return list;
}
// path は dialog プラグインの open() などで選ばせる。戻り値はジョブ ID
export async function printFile(path: string, copies = 1): Promise<number> {
if (!select.value) throw new Error('プリンターがありません');
return invoke<number>('print_file', { printer: select.value, path, copies });
}
// ラベルプリンターの命令(ここでは ZPL)をそのまま送る
export async function printLabel(text: string): Promise<number> {
const data = new TextEncoder().encode(`^XA^FO40,40^A0N,40,40^FD${text}^FS^XZ`);
return invoke<number>('print_raw', { printer: select.value, data });
}
印刷するファイルは ファイルを開くダイアログを表示する の open() で選ばせると、パスの文字列がそのまま渡せます。
2. バックエンドから実装する (Rust)
一覧と既定のプリンター
get_printers() は Printer の一覧を返しますが、そのままでは JS に返せないので、必要な項目だけの構造体に詰め替えます。name は Windows では system_name と同じで、macOS・Linux ではプリンターの説明文(空のこともある)です。印刷先の指定には常に system_name を使います。
同期のコマンドは画面と同じスレッドで動くため、応答の遅いネットワークプリンターがあると、その間ウィンドウが固まることがあります。async コマンドにして spawn_blocking に移します(重い処理を別スレッド・非同期で実行する)。
Windows では、既定のプリンターと名前の先頭の文字が同じプリンター(同じメーカーの FAX など)も is_default が true になることがあります。例では 2 台以上なら既定を決めつけず、ユーザーの選択に任せます。
ファイルやバイト列を送る
print_file() はファイルを、print() はバイト列を送り、ジョブ ID を返します。macOS・Linux では CUPS が PDF や画像を印刷できる形に変換します。Windows では中身がそのままプリンターに届くので、PDF はプリンターが PDF を直接受け付ける場合しか正しく印刷されません。
raw_properties の copies はどの OS でも部数になります。エラー型の PrintersError は to_string() できないので、message を取り出して返します(Result 型を使ってエラーハンドリングする)。
use std::path::Path;
use printers::common::base::job::PrinterJobOptions;
use printers::common::base::printer::{Printer, PrinterState};
use printers::common::converters::Converter;
use serde::Serialize;
/// JS に返すプリンターの情報
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PrinterInfo {
name: String,
system_name: String,
is_default: bool,
state: &'static str,
}
fn state_text(state: &PrinterState) -> &'static str {
match state {
PrinterState::READY => "ready",
PrinterState::PRINTING => "printing",
PrinterState::PAUSED => "paused",
PrinterState::OFFLINE => "offline",
PrinterState::UNKNOWN => "unknown",
}
}
#[tauri::command]
async fn list_printers() -> Result<Vec<PrinterInfo>, String> {
let list: Vec<Printer> = tauri::async_runtime::spawn_blocking(printers::get_printers)
.await
.map_err(|e| e.to_string())?;
// Windows では既定でないプリンターも is_default になることがあるので、1 台のときだけ信じる
let defaults = list.iter().filter(|p| p.is_default).count();
Ok(list
.iter()
.map(|p| PrinterInfo {
name: if p.name.is_empty() { p.system_name.clone() } else { p.name.clone() },
system_name: p.system_name.clone(),
is_default: p.is_default && defaults == 1,
state: state_text(&p.state),
})
.collect())
}
/// ファイルを指定したプリンターへ送り、ジョブ ID を返す
#[tauri::command]
async fn print_file(printer: String, path: String, copies: u32) -> Result<u64, String> {
tauri::async_runtime::spawn_blocking(move || {
let target = printers::get_printer_by_name(&printer)
.ok_or_else(|| format!("プリンターが見つかりません: {printer}"))?;
// 印刷キューに出る名前。省略すると Windows では数字だけになる
let job_name = Path::new(&path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "My App".into());
let copies = copies.clamp(1, 99).to_string();
let props = [("copies", copies.as_str())];
let options = PrinterJobOptions {
name: Some(&job_name),
raw_properties: &props,
converter: Converter::None,
};
target.print_file(&path, options).map_err(|e| e.message)
})
.await
.map_err(|e| e.to_string())?
}
// 変換させずにそのまま送る指定。Windows と CUPS で書き方が違う
#[cfg(windows)]
const RAW_FORMAT: &str = "RAW";
#[cfg(not(windows))]
const RAW_FORMAT: &str = "application/vnd.cups-raw";
/// レシート・ラベルプリンター向けに、バイト列をそのまま送る
#[tauri::command]
async fn print_raw(printer: String, data: Vec<u8>) -> Result<u64, String> {
tauri::async_runtime::spawn_blocking(move || {
let target = printers::get_printer_by_name(&printer)
.ok_or_else(|| format!("プリンターが見つかりません: {printer}"))?;
let props = [("document-format", RAW_FORMAT)];
let options = PrinterJobOptions {
name: Some("My App label"),
raw_properties: &props,
converter: Converter::None,
};
target.print(&data, options).map_err(|e| e.message)
})
.await
.map_err(|e| e.to_string())?
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![list_printers, print_file, print_raw])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
動作確認
npm run tauri dev で起動し、DevTools のコンソールで await loadPrinters() を実行します。Windows での戻り値は次のようになります(抜粋)。5 台で 0.1 秒ほどでした。
[
{ name: "OneNote (Desktop)", systemName: "OneNote (Desktop)", isDefault: false, state: "ready" },
{ name: "Microsoft Print to PDF", systemName: "Microsoft Print to PDF", isDefault: false, state: "ready" },
{ name: "Office Laser Printer", systemName: "Office Laser Printer", isDefault: true, state: "ready" }
]
printFile() の戻り値はジョブ ID で、印刷キューにはジョブ名(例ではファイル名)で表示されます。
よくあるエラーと対処法
- 「
PrintersErrordoesn't implementstd::fmt::Display」:map_err(|e| e.to_string())と書いたときのコンパイルエラーです。e.messageを返します。 - macOS で
window.print()を呼んでも何も起きない: コンソールに「webview.print not allowed. Permissions associated with this command: core:webview:allow-print」が出ていれば権限の追加漏れです。リリースビルドでは「Command plugin:webview|print not allowed by ACL」だけになります。 - Windows で PDF が印刷されない・意味の分からない文字が並ぶ: プリンターが PDF を解釈できていません。帳票なら HTML で作って
window.print()するか、下記の OS の関連付けを使います。 - 一覧が空の配列になる: エラーにはならず空で返ります。印刷スプーラーや CUPS が動いているか、プリンターが OS に追加されているかを確かめます。
- Linux のビルドで cups のライブラリが見つからない趣旨のリンクエラー:
libcups2-devなどを入れます。
OS ごとの違いと注意点
- Windows:
raw_propertiesで効くのはcopiesとdocument-formatだけです。PDF を普通のプリンターに出したいなら、PDF を開くアプリの印刷機能を PowerShell のStart-Process -FilePath <パス> -Verb Printで呼ぶ方法もあります(PowerShell スクリプトを実行する)。既定のアプリが対応していないと失敗し、印刷先は通常は既定のプリンターです。 - macOS / Linux:
raw_propertiesにはsides=two-sided-long-edge(両面)やmedia=A4など、lp -oと同じ CUPS のオプションを書けます。print()はバイト列を一時フォルダーのファイルにしてから送り、そのファイルは残ります(2.3 系)。 - macOS:
window.print()だけ権限が要ります(前提条件を参照)。 - iOS / Android: このレシピの方法は対象外です。printers クレートはデスクトップの OS 向けです。
