プリンターの一覧を取得して印刷する

printers クレートでプリンターの一覧と既定のプリンターを取り、ファイルやバイト列をダイアログなしで送る。画面を印刷する window.print() との使い分けと、Windows ではデータがそのまま届く点も示す。

ハードウェア連携 対象: Tauri 2.x 更新日: 読了目安: 約10分 hw-010
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 画面を印刷する(window.print())
  4. Rust で取った一覧から印刷先を選ぶ
  5. 2. バックエンドから実装する (Rust)
  6. 一覧と既定のプリンター
  7. ファイルやバイト列を送る
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

帳票やレシートを紙に出す方法は 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 で、印刷キューにはジョブ名(例ではファイル名)で表示されます。

よくあるエラーと対処法

  • 「PrintersError doesn't implement std::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 向けです。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する