認識列印系統架構


雖然現在許多版本的 linux 都有 printtool 等等工具可以自動設定印表機, 但運氣不好的時候還是會遇到無法自動偵測的機型 -- 例如我的 HP deskjet 3535。 更糟糕的是, 現代的列印系統非常複雜, 但出現的錯誤訊息卻不一定很充分, 有時候系統沒有任何錯誤訊息, 甚至連 log 檔也找不到有用的訊息, 但就是印不出東西。

這篇文章是筆者在 HP 工程師協助下, 解決自身問題後的副產品, 希望可以幫助讀者認識 Linux 列印系統的架構。 簡單的 GUI 設定後面究竟發生了那些事? foomatic 與 cups 之間又有什麼關係? 又或者你的 Linux 無法列印, 卻又沒有錯誤訊息, 不知下一步該如何, 這篇文章或許也有幫助。 至於找到出問題的環節之後, 應該如何解決, 則不在此篇討論之列, 因為筆者自己並不熟悉硬體。 不過有心的讀者可能因此而比較容易針對問題焦點, 知道應該閱讀那些相關文件, 或是用什麼關鍵字上 google 查; 又或者上網提問題時, 可以把你的狀況描述得比較清楚, 不至於像筆者當初完全茫然沒有頭緒。 此外, 筆者並不熟悉 OO.o 或 Mozilla 等等軟體的列印, 所以可以列印之後, 如何讓輸出更美觀, 也不在本篇討論之列。

要找到出問題的環節, 最簡單的方式就是盡量避開列印系統當中一層又一層的中間軟體, 單刀直入, 用最直接 (雖然未必是最簡單) 的方法列印。 確定印表機可以驅動之後, 再把較上層, 為簡化使用者介面設計的軟體加進去。

陽春列印法

對於我們來自 parallel port 介面時代的人而言, 現代的印表系統顯得太複雜, 也因而難以理解。 Parallel port 介面的古董機型, 完全不需要驅動程式就可以直接列印文字檔: cat 某文字檔名 > /dev/lp0

這種方法只能印英文文字檔。 如果是 .html .xml 或是程式檔等等 (也都算是文字檔), 印出來的就是未經處理的原始碼, 就像在 nano 或記事本底下看到的一樣陽春。 如果裡面有中文, 那麼你會看到亂碼, 不過段落章節還可以分辨得出來。 用這種方式列印其他檔案 (例如圖檔) 有可能讓你的印表機暫時發瘋, 有時必須關掉再開。

如果出現 Permission denied, 請用 ls -l /dev/lp0 看看是否需要改用 root 身份下 cat 指令。 通常一般使用者是沒有權限直接讀寫印表機的。

如果出現 "device or resource busy" 之類的訊息, 表示有其他軟體佔據著印表機埠。 這時可以下 lsof /dev/lp0 檢查究竟是誰在佔用。 順便一提, lsof 指令非常有用, 大力推薦安裝這個套件 (就叫做 lsof); 這個指令可能放在 /usr/sbin/ 底下。

如果印出來好像階梯一樣, 一路往右偏, 那是因為 Linux 下的換列字元是 \n, 但是印表機可能期待 DOS 的檔案, 以為換列字元是 \r\n (^M ^J)。 可以這樣修正: perl -pe 's/$/\015/' 某文字檔名 > /dev/lp0

這種方法看似無用, 不過它完全不需要安裝軟體, 所以可以拿它來確認你的硬體正常運作。 如果連這樣都印不出來, 那麼先別急著安裝設定複雜的軟體, 倒是應該先檢查連線是否正常, 或是把這部印表機改接到其他機器上試印, 確認一下硬體沒有問題。

Ghostscript: 萬能的印表機驅動程式

[ghostscript and friends] 在 linux 下, 非文字類型的檔案 (例如圖檔) 幾乎都是先將轉成 postscript (.ps) 檔 然後用 ghostscript (gs) 印出。 如果拿上述的 cat 方式餵現代的 usb 介面印表機, 通常都沒有反應也沒有錯誤訊息, 所以 gs 算是最直接的測試方法了。

例如要印一個中文文字檔 syllabus.txt, 先下: bg5ps < syllabus.txt > syllabus.ps 產生 .ps 檔; 英文文字檔則可以用 a2ps 轉成 .ps 檔。 這兩支程式都可在 rpmfind 找到; 說不定你的 Linux 光碟上本來就有。 可以先用 ghostview (gv) 開啟 .ps 檔看一下, 然後再根據你的印表機型號下 gs 指令:

    # 一部舊式, parallel port 介面的 HP DeskJet 400
    gs -q -dBATCH -dNOPAUSE -sOutputFile=/dev/lp0 \
        -sDEVICE=deskjet syllabus.ps

    # 一部 usb 介面的多功能事務機 HP OfficeJet 4200
    gs -q -dBATCH -dNOPAUSE -sOutputFile=/dev/usb/lp0 \
        -sDEVICE=ijs -sIjsServer=hpijs -dIjsUseOutputFD \
        -sDeviceManufacturer="HEWLETT-PACKARD" \
        -sDeviceModel="officejet 4200" syllabus.ps

當然你的印表機型號可能與我的不同, 所以 -sDEVICE 後面可能需要指定不同的字串。 請下 gs --help | less 查看有那些型號字串可以選擇。

至於 .jpg, .png, ... 等等各種圖檔, 則可用 netpbm 套件當中的對應指令, 先轉成 pnm 格式 (其實是 pbm, pgm, ppm 的概稱), 再用 pnmtops 轉成 ps, 即可列印。

目前所有印表機廠商當中, 對 Linux 的支援最友善/完整/公開的廠商, 應屬 hp。 他們的 hpijs 驅動程式是自由軟體, 支援 幾乎所有型號 的 hp 噴墨印表機。 當你在 gs 命令列上指定 -sDEVICE=ijs 時, gs 變成只是一個前端; 大部分的工作其實都透過 ijs 介面, 由外部程式 hpijs 在做。 在 Mandrake 底下, 安裝 printer-filters 套件, 裡面就有 hpijs。 rpmfind 底下也可找到單獨包裝的 hpijs 套件。 透過 ijs 介面躲在 ghostscript 後面服務的驅動程式, 還有 GIMP-Print 等等。

如果印不出東西, 可以試著分兩階段: 先用 ghostscript 列印到檔案裡面 (例如將 -sOutputFile 的參數改成 /tmp/output.prn), 再將這個檔案印到印表機裝置 (例如 cat /tmp/output.prn > /dev/usb/lp0) 看看問題到底出現在應用軟體層次以上 (gs 與 hpijs), 還是作業系統層次以下。 (欠缺 kernel module?)

Foomatic: 型號與驅動方式的對照表 及 對照表管理員

每次列印都要下那麼多參數, 有點麻煩。 如果你的電腦 (直接或透過網路) 接上好幾部印表機, 或是經常帶著筆記電腦在不同的地點使用不同的印表機, 就更頭大了。 如果可以為每部印表機取個名字, 並且只設定一次那些複雜的參數, 從此以後列印時, 只指定印表機名稱就好了, 這樣不是比較簡單嗎? Foomatic 的最基本功能就在提供這個服務。 它有一個很大的對照表, 記載這每一種印表機需要使用的驅動程式與參數。 每部印表機稱為一個 queue, 通常必須設定四個參數:

「型號」 與 「驅動程式」 兩個字串是怎麼找到的呢? 下這個指令: foomatic-configure -O | less 在裡面搜尋你的印表機型號, 它的 id 欄及 driver 欄就是了。 以我另一部印表機 HP OfficeJet 4200 為例, 搜尋 "4200" 得知這個型號的 id 是 "HP-OfficeJet_4200", 而 driver 是 "hpijs"。 然後:

        foomatic-configure -s direct -Q # 增加印表機之前, 先查詢一下。
        foomatic-configure -s direct -n hp4200 -c usb:/dev/usb/lp0 \
            -p HP-OfficeJet_4200 -d hpijs
        foomatic-configure -s direct -Q # 增加印表機之後, 再查詢一下。
        foomatic-configure -s direct -n hp4200 -D
        foomatic-configure -s direct -Q # 指定內定印表機之後的確認。

這裡的 -Q 是查詢; -D 是將之設為內定的印表機: 將來列印時, 如果不指明印到那一部, 就自動印到這一部。 從此以後, 可以這樣簡單列印: bg5ps < syllabus.txt | foomatic-rip 如果要從另外一部印表機列印, 只要在 foomatic-rip 後面用 -n 指定印表機名稱就可以了。 也就是說, foomatic 最大的功用就是簡化命令列 -- 使用者指定印表機名稱, foomatic 負責填入 gs (或是其他驅動程式) 命令列上的許多細節。

[foomatic: 對照表及管理員]

如果 gs 直接印沒有問題; 用 foomatic-rip 印卻無聲無息地失敗, 可以先將上面的 -c usb:/dev/usb/lp0 改成 -c stdout, 令 foomatic-rip 印到終端機視窗。 此時若不小心直接下 foomatic-rip 列印, 螢幕會接收到印表機的控制碼而亂掉。 這時可以下 reset 指令恢復正常; 如果還是亂碼, 可以再試試看 perl -e 'print chr(15)'。 當然我們的目的其實是要將列印動作分成兩步, 以便找出究竟是那一步出了問題:

        foomatic-rip syllabus.ps > /tmp/syllabus.prn
        cat /tmp/syllabus.prn > /dev/usb/lp0

像我就曾經因此而發現 "device or resource busy", 再用上面所說的 lsof 查看 /dev/usb/lp0 而發現原來是先前實驗的 hpoj 套件佔據了印表機埠。

Foomatic-configure 與 foomatic-rip 都是 perl script, 如果有必要 (例如覺得手冊寫得不夠詳盡), 也可以直接去讀它的原始碼; 甚至於光是從註解就可以找到有用的資訊, 像是上面的 -c stdout 就是在註解裡面發現的。 這就是自由軟體的好處 :-)

其實可以驅動一部印表機的驅動程式 (driver) 可能不只一套軟體, 例如有很多印表機既可用 gs 驅動, 也可用 gimp-print 驅動。 不同的驅動程式, 在不同的列印狀況下, 效果可能有高下之別。 如果你不滿意內定的驅動程式, 可以在 foomatic-configure -O | less 裡面找到 drivers 一欄, 改用其他驅動程式當做 設定時 -d 的參數。 細節請見 foomatic 的 USAGE 文件。

至於 -s direct 是什麼意思呢? 那就要談到 spooling 了。

Spooler: 多人/多機共用印表機的救星

反過來說, 如果許多人/許多部電腦共用一部印表機, 會發生什麼問題? 可能兩份文件緊接著到達, 前一份還沒有印完, 後一份的資料就到了。 如果沒有處理好, 兩份文件恐怕就要 「你儂我儂」 纏夾不清了。 因此需要有一支程式擋在應用軟體與印表機驅動程式之間, 令所有的列印工作 (稱為 print jobs) 按順序排好, 一整份印完才開始印下一份。 這支程式就叫做 spooler; 這也是為什麼每部印表機稱為一個 printer queue。 如果你習慣同時從好幾個應用軟體裡面列印, 即使印表機只有你一個人在用, 也不宜將列印工作直接導入硬體 (/dev/usb/lp0), 而應該交由 spooler 安排時間列印。

話說回來, 如果印表機由一人獨享, 而且使用者總是習慣性地耐心等一個檔案印完才開始印下一個檔案, 其實就不需要再安裝更多軟體了 -- 這時使用者本身就扮演 spooler 的角色 :-) 像我就習慣如此, 所以上面設定 foomatic 時, 用 -s direct 指定不使用任何 spooler, 是我較常用的列印方式。

lpd 是最古老的 spooler; 新的 spooler 有 CUPS, LPRng, PDQ, ...等等。 你只需要安裝其中一套。 本文以 CUPS 為例, 來說明 spooler 與 foomatic 的關係。

CUPS

[CUPS: 多功能 spooler] Common Unix Printing System (CUPS) 是目前最常用的 spooler 之一。 請確認一下已安裝 cups 套件。 然後用 root 的身份啟動 cups 服務: service cups start 它的設定介面叫做 cups web interface, 使用方式很簡單, 用瀏覽器打開 http://localhost:631 就可以操作。 選擇 "Do Administration Tasks", 用 root 的帳號密碼登入。 然後:

  1. 選擇 "Manage Available Printers": 如果這是第一次使用 cups, 此時應該沒有看到任何印表機。
  2. 選擇 "Add a New Printer": 這裡的欄位都可以隨意填寫, 自己要記得就是了。
  3. 選擇 "Device" (硬體裝置): 如果是 usb 印表機, 應該會在 USB Printer #1 後面看到系統偵測到的印表機回應字串。 不要高興得太早, 還不一定可以驅動。
  4. 選擇 "Make" (廠牌)
  5. 選擇 "model" (型號): 且慢按下 "Continue", 先用 foomatic-configure -s cups -Q 查一下 cups 目前的設定, 並且用 ls /etc/cups/ppds/ 查看目錄裡面有沒有東西, 又用 less /etc/cups/printers.conf 查看目前 cups 認得本機上那些印表機。 按下 "Continue" 之後, 再查一次, 看看有何變化。

如果找不到適合的型號, 不必勉強將就。 可以到 linuxprinting 網站瀏覽完整的 印表機列表。 請找到正確的型號, 下載它的 ppd 檔。 用 gzip 壓縮, 並放到 /usr/share/cups/model 的某個子目錄底下 (依據它的廠牌)。 必須重新啟動 cups, 並且用瀏覽器重新登入, cups 才知道有新的 ppd 可以用。

或者不從網站下載, 直接用 foomatic 替 cups 產生 ppd [1] [2] 更簡單: foomatic-configure -s cups -n hp4200 -c usb:/dev/usb/lp0 -p HP-OfficeJet_4200 -d hpijs 這背後其實已經發生了三件事情:

  1. foomatic 從它資料庫當中對應的 xml 檔產生出 ppd 檔
  2. 不經過 cups 的瀏覽器介面, 而是直接把 ppd 檔放到 /etc/cups/ppds/ 目錄下
  3. 不經過 cups 的瀏覽器介面, 而是直接修改 /etc/cups/printers.conf 告知 cups 有一部新的印表機

所以只需要重新啟動: service cups restart, 不需要再設定, cups 就會認得了, 可以再用瀏覽器進去確認看看。 也就是說要設定 cups, 可以選擇用 foomatic 或用 cups web interface。 當然後者專門搭配 cups 而設計, 所以選項較多; 但是單就設定驅動程式而言, foomatic 可以說是天下無敵。

不論是用那一種方式設定, 完成之後可以下 lpq 指令查看系統內印表機的狀態; 下 lpr 指令列印檔案; 下 lprm 指令刪除尚未列印完的 print job。 詳見 cups 套件的許多相關文件。 這種透過 spooler 列印的方式, 你可以從好幾個應用軟體甚至好幾部電腦放心地同時列印; 在下完 lpr 指令之後, 甚至直接刪除 .ps 檔也沒有關係, 因為 spooler 已經把檔案複製到系統的 printer queue 去了。

如果細心查看, 會發現 /usr/bin/lpr 其實指向 /etc/alternatives/lpr 而後者又指向 /usr/bin/lpr-cups 其他類推。 為什麼要這麼麻煩呢? 前面說過, unix 底下的 spooler 系統有很多套。 這樣的安排, 方便系統管理員很容易將 CUPS 換成 LPRng, 或將 PDQ 換成 CUPS, 系統只需要改 symbolic link 就好; 至於使用者則不需要知道究竟用的是那一套 spooler, 可以一直用相同的列印指令。

事實上 cups 不僅僅是一個 spooler, 底下牽涉的協助軟體也不只有 ghostscript 及眾多 ps 轉換程式。 它還可以:

  1. 假扮網路 postscript 印表機 (即使真實的印表機不支援 postscript)
  2. 列印各種文字檔/圖形檔/...
  3. 將輸出分組, 每組有不同的設定 (例如每幾頁合印在一張紙的兩面等等複雜規則)

不過筆者也是外行。 Kurt Pfeifle 所寫的 "Dissecting The CUPS Filtering System: A Network Postscript RIP For non-PS Printers" 畫出架構圖, 一目瞭然, 特別大力推薦給有勇氣深究 cups 架構的朋友。

結語

今日的 GNU/Linux 作業系統日趨成熟, 系統與硬體設定方式也越來越簡單, 和 MS Windows 一樣有 GUI 介面; 但是筆者仍舊偏好使用命令列或文字選單工具。 這是因為這類工具對於特殊環境 (例如 KDE 或 GNOME) 依賴性較低, 吃的資源較少, 而且較容易自動化 (透過 scripting)。 如果讀者與我一樣希望可以在各種版本的 Linux 之間自由切換穿梭, 卻又懶得為同一項工作 (例如設定印表機) 學那麼多不同的選單, 筆者大力推薦這種學習方式。 這並不表示我們需要記很多指令 -- 把實驗出來的心得寫成筆記, 或是直接寫入 shell script, 甚至像這樣寫成文章投稿, 既可省去自己記憶的麻煩, 也可同時嘉惠他人, 開始對惠我良多的自由軟體界做一點微薄的回饋, 感覺很棒。 (運氣好的話, 還可以順便賺稿費 :-)

對於這類技術文件的寫法, 筆者也有一點意見。 介紹 GUI 的文件, 經常太低估讀者, 描述太多操作細節; 另一方面有些文件似乎又太高估讀者, 寫得很像 man page, 列出所有指令, 及許多很少用到的選項, 卻較少解釋那種場合需要用那個指令。 筆者認為 HOWTO 文件 "任務/觀念導向" 的寫法非常值得參考, 對於願意動腦但無力深究的人最有幫助。 下每個指令時, 給讀者一點動機 ("我們為什麼要下這個指令?") 至於細節, 可以留給 man page。 如果能夠再加上 trouble shooting 的流程圖, 或解釋系統架構的觀念圖, 就更好了。 筆者正在朝這個方向努力。

最後要感謝 hpijs 的 David Suffield 迅速, 耐心地來回數次回答我許多問題, 終於找出原因 -- 原來 3535 可以選 3528 的驅動程式。 新版的 foomatic 對照表裡面將反應這點。 希望這篇文章不只有助於解決印表機的問題, 也可以促使讀者多多向軟體作者回報自己的問題, 像筆者一樣用 bug report 的方式貢獻自己微薄的力量, 加入自由軟體利已利他的行列! 至於如何發問才不會浪費軟體作者時間, 這又是另一個話題, 以後有機會再談囉。

相關網站及更多資訊

  1. http://www.linuxprinting.org/: Linux 列印文件的大本營, 本文參考此處多份文件
  2. http://netpbm.sourceforge.net/: 用 2n 套轉換程式解決 n*n 個圖檔轉換問題的圖檔處理工具集
  3. http://www.linuxprinting.org/ijs/: 讓 ghostscript 可以外掛驅動程式的介面
  4. http://hpinkjet.sourceforge.net/: HP 的開放原始碼印表機驅動程式, 採用 ijs 介面附掛在 ghostscript 之下

自己電腦裡面 /usr/share/doc/ghostscript*/ 裡面的文件也很值得參考, 特別是 Use.html。