おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

foomaticとはなんじゃらほい

必殺、「オープンソースの印刷について昔からやってらっしゃる詳しい人がブログとか書いてないのをいいことに、検証がめんどくさい半端知識を書き散らすエントリ」第……何弾だっけ? まあいいや。


今日は Till Kamppeter の名前を有名にした二つ (一つは LinuxPrinting.org) のウチ一つ、foomatic を取り上げまする。

foomatic ってどういう意味?

変な名前ですよね。foo ってのはあの foo bar baz の foo ですな。
実はこれ、元々の名前は cups-o-matic って名前で、「いや別に CUPS 以外でも使えるやろ」ってことで名前を変えたということらしいす。当然10年以上も前のことなんでリアルタイムで知ってるわけじゃないですけど、本家ページWhy the funny name? (なんでこんな変な名前なの?)ってエントリーがわざわざ作ってあって説明が書いてあります。

でまあ、最初は、Grant Taylor って人が、「CUPS はプリンターの情報を全部 PPD に書くってアプローチだけど、プリンターのいろんな情報ならオレもう持ってるぞ。だからそれから CUPS の PPD を生成するようにしたらいいんじゃね?」「ついでに、CUPS と GS の依存性も切り離して、GS が新しいプリンターに対応してなくても PPD だけで新しいプリンターが使えるようになったらいいんじゃね?」というアイディアを出したみたいすね。これが最初の cups-o-matic。

多分 CUPS の dsc ってファイルも「PPD 文法めんどいからもっと手軽に定義できるように」って作られたものだと思うけど、みんな使ってんのかなあ。少なくとも私は使ったことないですな……。

で、Till Kamppeter が「データベースから PPD 生成するならさ、lpr のフィルターとしても、ほかの (例えば Solaris 10 や HP-UX のデスクトップとか) フィルターとしても、スプーラレスで使える場合でも、適切なデータ生成してそれぞれのフィルターに食わせればよくない?」っていっていろんなフィルター考えて、じゃあもう名前一個にしちゃおう! というのが foomatic なんだそうな。

歴史的経緯はともかく、foomatic は巡り巡って今はほとんど CUPS ベースの印刷システムにしか使われてないですね。

foomatic の内容

某雑誌に「一般に foomatic といえば foomatic-rip のコトを指す」って書いちゃったけど、上に書いたとおり、オリジナルとしては DB に記載された内容から PPD (以外でも理屈上ではイケルけど) を生成するって仕組み foomatic-db がまずあり、それで生成した PPD をごにょごにょする仕組みが foomatic-rip があると。

さらっと説明します。あくまでもさらっと。なぜかっていうと foomatic-db よくわかんないからw

foomatic-db

すごく単純に行っちゃうと XML で書かれた機種情報を読み込んで PPD を生成するっつー仕組み、でいいと思います。機種情報は一箇所に固まってればいいのでそれが前回も紹介した OpenPrinting Database で、その前身が linuxprinting.org というサイトです。
このデータベース、すごく単純にいっちゃえば、「誰でも、自分が持ってたりなんだりするプリンターの情報をアップロードして集積すればハッピーじゃんか」というものでして、ベンダーとかじゃなくても、ぼくもあなたも自由にアップロードできちゃいます*1。そのときに、「PPD をそのまんまアップロードする」ことと「XML の機種情報をアップロードする」ことと選べると。
前に書いたと思いますが、CUPS は PPD に書いてある情報、例えば「このプリンターには InputSlot(給紙トレイ) という選択肢があってね、InputSlot には Tray1(トレイ1), Tray2(トレイ2), Manual(手差し) があるんだよ」という感じのものから、UI に「給紙トレイ」という項目を表示し、その選択肢として「トレイ1」「トレイ2」「手差し」とかを出すわけです。んで、ユーザーが「手差し」を選んだら、CUPS は後段のフィルターパイプラインにこれをコマンドライン引数として渡します。こんなふうに。

-o InputSlot=Manual

これはフィルターパイプラインに延々渡されていって、そもそもの設計では GS なりなんなりが受け取ってよしなに処理する予定でした。……おっと、これは次の foomatic-rip の話だ。
話戻って、要はこういう「選択項目に何があって」「それはどういう見せ方をするもので (例えばブーリアンならチェックボックスの方がいいかもですよね)」「選択肢はこれこれ」って情報を書くとき、XML に抽象化しといたほうが PPD 以外にも使えていいじゃん、という発想ですね。ただ、実際今は CUPS 一択になっちゃったので、登録側の DB の方が PPD 直接アップロードサポートするようになった、と。
どっちが多いのかなあ。正直よくわかんないですけど、私は PPD が難しいとはあまり思ってないので、XML で書くほうがめんどくさいすね(^^; だから使ったことない。

蛇足ですが OpenPrinting Database で PPD をもらうときに「PPD の生成」みたいな UI になってるのはそこら辺の理由です。

foomatic-rip

さて次は foomatic-rip です。さっきも書いたように CUPS はユーザーの洗濯し田結果をオプションとして:

-o InputSlot=Manual

みたいな感じでフィルターに渡してきます。オプションの種類によっては中間のフィルターで処理されることもありますが、基本的には最終弾にいる、大抵は Ghostscript が受け取って:

「えーナニナニ、オレは今 LIPS 4V のドライバーで動いてて、InputSlot を Manual にしろって? じゃあ、このコマンド出すか」

みたいに処理する……というわけでした。
でもじゃあ、プリンター言語としては LIPS 4Vで印刷できるけど、通常の給紙トレイを6段に手差しを2段持ってるプリンター、とかいたらどうしましょう? 普通ドライバーを作るときにはそこまで知らんがな、という感じです。それを PPD でなんとかできれば……じゃあ、やりますか! というのが foomatic-rip。

foomatic-rip は GS の代わりに最終段のフィルターとして位置します。最終段のフィルターというのは本来はプリンターが食えるデータを作るものですが、foomatic-rip はそういう機能をもってません。
じゃあ何するかというと:

  • 最終段のフィルターとして渡されたデータを標準入力として、任意のコマンド文字列をパイプで実行する
  • そのときに、PPD の選択肢・選択項目によって変数の代入をすることができて、コマンド文字列を実行する前に、代入された変数を置換できる

ということ。

ここらへんSDさんにも書いたので深入りは避けたいけど、例えば某 PPD から例を抜き出すと、

*FoomaticRIPCommandLine: "(printf '\033%%-12345X@PJL SET COPIES=&copies;\n'%G|perl -p -e "s/\x26copies\x3b/1/");
(gs -q -dBATCH -dPARANOIDSAFER -dNOPAUSE -dNOINTERPOLATE %B%A%C %D%E | perl -p -e "s/^\x1b\x25-12345X//" | perl -p -e "s/\xc1\x01\x00\xf8\x31\x44/\x44/g");"
*End

...
*OpenUI *ColorModel/Color Mode: PickOne
*FoomaticRIPOption ColorModel: enum Composite B
*OrderDependency: 10 AnySetup *ColorModel
*DefaultColorModel: Color
*ColorModel Color/Color: "%% FoomaticRIPOptionSetting: ColorModel=Color"
*FoomaticRIPOptionSetting ColorModel=Color: "JCLDatamode=Color GSCmdLine=Color"
*ColorModel Grayscale/Grayscale: "%% FoomaticRIPOptionSetting: ColorModel=Grayscale"
*FoomaticRIPOptionSetting ColorModel=Grayscale: "JCLDatamode=Grayscale GSCmdLine=Grayscale"
*CloseUI: *ColorModel

*FoomaticRIPOption GSCmdLine: enum CmdLine B 10
*FoomaticRIPOptionSetting GSCmdLine=FromColorModel: ""
*FoomaticRIPOptionSetting GSCmdLine=Color: " -sDEVICE=pxlcolor"
*End
*FoomaticRIPOptionSetting GSCmdLine=Grayscale: " -sDEVICE=pxlmono"
*End

...

*OpenUI *PageSize/PageSize: PickOne
*FoomaticRIPOption PageSize: enum CmdLine C
*OrderDependency: 20 AnySetup *PageSize
*DefaultPageSize: A4
*PageSize A3/A3: "%% FoomaticRIPOptionSetting: PageSize=A3"
*FoomaticRIPOptionSetting PageSize=A3: " -sPAPERSIZE=a3 -sOutputFile=- - "
*PageSize A4/A4: "%% FoomaticRIPOptionSetting: PageSize=A4"
*FoomaticRIPOptionSetting PageSize=A4: " -sPAPERSIZE=a4 -sOutputFile=- - "
*PageSize A5/A5: "%% FoomaticRIPOptionSetting: PageSize=A5"
*FoomaticRIPOptionSetting PageSize=A5: " -sPAPERSIZE=a5 -sOutputFile=- - | perl -p -e "s/\xf8\x28\xc0.\xf8\x25/\xf8\x28\xc0\x0f\xf8\x25/g""
*PageSize A6/A6: "%% FoomaticRIPOptionSetting: PageSize=A6"
*FoomaticRIPOptionSetting PageSize=A6: " -dDEVICEWIDTHPOINTS=298 -dDEVICEHEIGHTPOINTS=420 -sOutputFile=- - &&
 | perl -p -e "s/\xf8\x28\xc0.\xf8\x25/\xf8\x28\xc0\x14\xf8\x25/g""
*End
...
*CloseUI: *PageSize

わけわかりませんな。順に見て行きましょう。
*FoomaticRIPCommand は名前の通り foomatic-rip が後段に渡すためのコマンドを記述するところです。少しばらして読みます。

(printf '\033%%-12345X@PJL SET COPIES=&copies;\n'%G|perl -p -e "s/\x26copies\x3b/1/");

クォートとか多いんで分かりにくいですが、要は:

  • 最初にプリンターに、PJL という「ジョブ情報を送るためのモードに切り替える」ための UEL という文字列 "%-12345X" を送ってます。
  • @PJL SET COPIES で印刷部数をセット。このとき &copies は CUPS フィルターにコマンドライン引数で渡される印刷部数に置換されます。
  • %G は省略。要は PPD の選択項目によって決まる何かが突っ込まれると思ってください。
  • で、これらがくっついた印刷データをパイプで perl に渡して、 印刷データの中にある copies の設定を 1 にしちゃいます*2。なぜかというと、印刷部数の設定というのは、ジョブの内部データとして指定する方法と、それをくるんでいる PJL でやる方法があるんですが、両方がまじってるとよくないし、PJL で設定するほうがいろいろうれしいからです。これは機会があればまた書きます。

さて次。

(gs -q -dBATCH -dPARANOIDSAFER -dNOPAUSE -dNOINTERPOLATE %B%A%C %D%E | perl -p -e "s/^\x1b\x25-12345X//" | perl -p -e "s/\xc1\x01\x00\xf8\x31\x44/\x44/g");"

最初はわかりやすいですね。Ghostscript をいろんな引数で呼び出してるだけです。ここで %B%A%C とかなってるやつは後述。
これをパイプで渡して perl で処理してるのは、さっき UEL の話をしましたよね。プリンターというのはだいたい次のような制御になってます。

  • UEL を食べて PJL を受け付ける用に鳴ってる状態
  • PJL として @PJL ENTER LANGUGAGE=xxx というコマンドを受け付けて、xxx というプリンター言語を受け付ける状態
  • 再び UEL を受け付けて PJL を待つ状態

いつまでもプリンター言語を受け付けておく状態にしておくと、PJL のつもりで投げた奴がなぜか印刷データになったりとかしてちょっち困るので、印刷が終わったら UEL を投げて PJL 状態に移っておきますよという意味。
つぎのパイプの処理はさっぱりわかりませんが、後述するようにこのデータは GS によって PCLXL というプリンター言語に変換されてます。その中の一部が邪魔なんで取ってるんでしょう。

という感じです。
では次は %B とかの説明。ようは変数置換です。

*OpenUI *ColorModel/Color Mode: PickOne
*FoomaticRIPOption ColorModel: enum Composite B
*OrderDependency: 10 AnySetup *ColorModel
*DefaultColorModel: Color
*ColorModel Color/Color: "%% FoomaticRIPOptionSetting: ColorModel=Color"
*FoomaticRIPOptionSetting ColorModel=Color: "JCLDatamode=Color GSCmdLine=Color"
*ColorModel Grayscale/Grayscale: "%% FoomaticRIPOptionSetting: ColorModel=Grayscale"
*FoomaticRIPOptionSetting ColorModel=Grayscale: "JCLDatamode=Grayscale GSCmdLine=Grayscale"
*CloseUI: *ColorModel

*FoomaticRIPOption GSCmdLine: enum CmdLine B 10
*FoomaticRIPOptionSetting GSCmdLine=FromColorModel: ""
*FoomaticRIPOptionSetting GSCmdLine=Color: " -sDEVICE=pxlcolor"
*End
*FoomaticRIPOptionSetting GSCmdLine=Grayscale: " -sDEVICE=pxlmono"
*End

まずは上のほう。FoomaticRIPOptionSettings 以外はごく普通の PPD の設定です。%% 以降は単なるコメントなのでスルーすると、「ColorModel という選択項目があって、デフォルトは Color で、選択肢としてはColorとGlayscaleがありますよということがわかればいいです。

そうすると次は簡単ですね。enmu CmdLine B 10 というのは「%B は次の選択肢の置き換えになるからね」という意味。最後の 10 は優先順位とかだったかなあ。
んで、*FoomatciRIPOpetionSetting GSCmdLine=Color: とかいうのは、上の選択肢で Color が選ばれたときは %B に何を置換するのかということ。この場合は -sDEVICE=pxlcolor だということになりますね。

ここまで見ていけばページサイズも楽勝でしょう。

*OpenUI *PageSize/PageSize: PickOne
*FoomaticRIPOption PageSize: enum CmdLine C
*OrderDependency: 20 AnySetup *PageSize
*DefaultPageSize: A4
*PageSize A3/A3: "%% FoomaticRIPOptionSetting: PageSize=A3"
*FoomaticRIPOptionSetting PageSize=A3: " -sPAPERSIZE=a3 -sOutputFile=- - "
*PageSize A4/A4: "%% FoomaticRIPOptionSetting: PageSize=A4"
*FoomaticRIPOptionSetting PageSize=A4: " -sPAPERSIZE=a4 -sOutputFile=- - "
*PageSize A5/A5: "%% FoomaticRIPOptionSetting: PageSize=A5"
*FoomaticRIPOptionSetting PageSize=A5: " -sPAPERSIZE=a5 -sOutputFile=- - | perl -p -e "s/\xf8\x28\xc0.\xf8\x25/\xf8\x28\xc0\x0f\xf8\x25/g""
*PageSize A6/A6: "%% FoomaticRIPOptionSetting: PageSize=A6"
*FoomaticRIPOptionSetting PageSize=A6: " -dDEVICEWIDTHPOINTS=298 -dDEVICEHEIGHTPOINTS=420 -sOutputFile=- - &&
 | perl -p -e "s/\xf8\x28\xc0.\xf8\x25/\xf8\x28\xc0\x14\xf8\x25/g""
*End
...
*CloseUI: *PageSize

要は例えば A4だったら %C が "-sPAPERSIZE=a4 -sOutputFile= -" って置き換わるってだけです。
ん? じゃあ A6 のやたら長いのはなにかって? これは pxlcolor の仕様として、sPAPERSIZE=A6 ってのがないんです。ので、オプションでデバイスのサイズを渡してやった上、PCL XL のコマンドで A6 にするように置換してると。

さっきの GS のコマンドに、ためしにこれらを入れてみましょう。%B, %C 以外は省略してます。あと実態参照はめんどくさいので書き直しました。

gs -q -dBATCH -dPARANOIDSAFER -dNOPAUSE -dNOINTERPOLATE -sDEVICE=pxlcolor -dDEVICEWIDTHPOINTS=298 -dDEVICEHEIGHTPOINTS=420 -sOutputFile=- - | perl -p -e 's/\xf8\x28\xc0.\xf8\x25/\xf8\x28\xc0\x14\xf8\x25/g'

ということで foomatic-rip 自体は「選択肢に併せて FoomaticRIPcommandLine の文字列置換してそれを叩くだけ」という単純なことしかしてないんですけど、要はパイプでデータ加工しほうだいなのでものすごく柔軟なことができちゃいますよと。

終わりに

いやーやっぱり foomatic (特に rip) 便利だわ。
一部の機能は CUPS に取り入れられてるとはいえ、やっぱコイツがないと柔軟なドライバー開発はできませんな。ということでコントリビュートしろというのはなかなか難しいでしょうけど、まあ、感謝してくださいw

*1:これについてリコーの米国子会社 Ricoh Americas Corporation が非常に良い取り組みをしているというのは過去に書いた。

*2:なんで単なる置換なのに sed じゃなくて perl なのかは知りません。