書いた人:須藤功平
2024/01/17 更新: 北市真 脚注リンクと画像位置、リンク切れの修正、及びコードブロックのハイライト
どちらの画像の方がきれいですか?
ここでは、文字や丸い線がなめらかで、色が透過していたりする方 がきれいだということにします。本稿で紹介する cairo の長所の 1 つ は、ここでいうきれいな画像を出力できるということです。
画像を PNG や PDF や SVG など複数のフォーマットで出力したくありませ んか?
異なるフォーマットで出力するために、全く別の出力モジュールを 作るのは面倒です。それぞれの形式で特有の部分 (例えば SVG のバー ジョンなど) だけ別々に作成し、線を描くなどの多くの部分を共有 すると楽です。つまり、出力フォーマットに依存しない高レベルな 描画 API を使用するということです。
cairo を使えばそれができます。rcairo を使えばそれが Ruby ででき ます。
cairo は 2 次元画像を描画するためのライブラリです。まず、cairo を用いて作成した画像を紹介します。
2 次元画像用のライブラリはいくつかありますが、どうして cairo を 使うのか。cairo を使う最大の理由はこれです。
cairo は Firefox や GTK+1など有名プロジェクトの描画周りのバックエ ンドとして採用されています。他にも、SVG 描画ライブラリ librsvg や PDF 描画ライブラリ poppler など各種描画ライブラリのバックエン ドとしても採用されています。
多くのプロジェクトに採用されていると、多くのフィードバックが あり、開発が進むという効果があります。このため将来的に安心し て使えます。
どうして cairo が多くのプロジェクトで採用されているのか。ここ では cairo のよさを紹介します。cairo の特長は以下の 3 点です。
cairo はアンチエイリアスをサポートしているため、文字や丸い線な どをなめらかな線で描画します。
cairo はベクトルベースの描画モデルを採用しています。この描画モ デルでは拡大・縮小しても画質が落ちません。
PNG や GIF などで使用している、各画素に色を割り当てて画像を描画 するモデルではそうはいきません。このモデルでは、拡大・縮小し た際に画素の数が増減します。画素の過不足を補うためにそれらの 画素の色に適当な色を割り当てて補う必要があります。この時に画 質が落ちる可能性があります。
図 lena.jpg が元の画像で、図 lena-resized.jpg が 1/4 に縮小してから 4 倍に拡大した画像です。画像がぼやけてしまっているのがわかりま す。これは、最初の縮小時に画素が欠落し、次の拡大時に欠落した 画素の色を周囲の画素の色から推測しているため、全体的に隣り合 う画素どうしが似た色になるためです。
lena.jpg | lena-resized.jpg |
---|---|
一方、ベクトルベースの描画モデルでは、各画素の色という情報で
はなく、どのように描画するかという情報を持っています。各画素
にどの色を割り当てるかは実際に描画するときに決定します。例え
ば、線を引く場合は点 A から点 B まで赤で線を引く、という情報を持っ
ていて、実際に描画するときに点 A から点 B までの間でどの画素に赤
を割り当てればよいのかを計算します。
このようにすることで画素の色を適当に推測する必要がなくなり、 拡大・縮小しても画質を落とさずに描画することができます。
他にも、描画オブジェクトをどの程度透過させるかを指定するアル ファチャンネルやグラデーションなど高品質な出力をサポートする 機能があります。
描画コンテキストとは描画関係の情報を管理するオブジェクトです。 この描画コンテキストを用いた API で描画するときは以下のような流 れになります。
例えば、線を描く場合はこうなります。
Ruby で書くとこうです。
def draw_line(start_x, start_y, end_x, end_y)
move_to(start_x, start_y)
line_to(end_x, end_y)
stroke
end
点がどのように移動してきたかは描画コンテキストが知っています。 move_to や line_to をすると、描画コンテキストには「ここに移動し た」、「あそこまでまっすぐに移動した」などという情報が追加さ れます。このような点が動きまわった経路をパスといいます。 「線を描く」といった描画処理はパスに対して行います。 stroke に引数がないのはそのためです。
この API では、「ここからあそこまで線を描く」というそのものの API がなく、単に線を描くだけなのに 3 つの操作が必要になります。 面倒ではありませんか2。そ れでは、この描画コンテキストを用いる方法の利点はなんでしょう か。
描画コンテキストはパスを管理するだけではなく、変換行列も管理 しています。この変換行列というものを使えば、パスを作る部分を 変更することなく、パス全体を移動させたり、回転させたり、拡大 縮小したりできます。例えば、点(20, 40)から点(20, 80)までの線 を描くには以下のようになります。
move_to(20, 40)
line_to(20, 80)
stroke
これを、全体的に右に 40 分ずらして、点(60, 40)から点(60, 80)ま
での線を描くようにします。
変換行列を使わず、単純にやる場合はこうなるでしょう。
move_to(60, 40)
line_to(60, 80)
stroke
変換行列を使えばこうなります。
translate(40, 0)
move_to(20, 40)
line_to(20, 80)
stroke
translate で x 座標だけを右に 40 分ずらしています3。注目してほしいのは move_to と line_to の引数が変わっていないところです。 これは描画処理の部分をまったく変更せずに描画される結果全体を 移動させたり、回転させたり、拡大縮小できるということです。
たとえば、以下のように常に描画領域が 100x100 であるとして描画 処理の部分を描くことができます。scale は拡大/縮小率を 設定します。
# width/height は実際の描画領域の幅/高さ
scale(width / 100.0, height / 100.0)
# width/height に関わらず左上から右下に対角線を描く
move_to(0, 0)
line_to(100, 100)
stroke
この機能はサイズの違う結果を複数生成したい場合に便利です。サ ムネイル画像を用意したい場合や、ウィンドウサイズに合わせて内 容を表示したい場合などです。
描画処理に cairo を利用すると再利用性が高くなります。cairo は
PNG や PDF などの画像フォーマットとして出力するだけではなく、
GTK+ や Windows、Mac OS X のウィジェットに描画することもできま
す。また、そのために move_to や stroke、
translate などといった描画関係の処理を変更する必要はあ
りません。出力先が画像フォーマットでも、ウィジェットでも同じ
ものを使用できます。これは cairo が描画内容を出力する部分を抽
象化しているからです。
これは Web アプリケーションでもデスクトップアプリケーションで も嬉しい機能です。Web アプリケーションでは、ブラウザ上でのプ レビュー用として PNG 形式の画像を返し、印刷用として PDF 形式の画 像を返すことができます。デスクトップアプリケーションでは、ウィ ジェットへの描画内容をスクリーンショットとして PNG や PDF 形式で 出力することができます。しかも、これらは描画処理をまったく変 更することなく実現できます。
cairo の欠点はなんといっても画像効果をサポートしていないこと でしょう。
画像効果とは、「ぼかし」や「照明」といったものです。
画像効果をサポートしていないと困る例をあげましょう。例えば、 「ぼかし」をサポートしていない場合はきれいな影を作ることが大 変です。少しずらしてグレーっぽい色を描画すれば影を作ることが できますが、ふわっとした影は簡単には作れません。Web 2.0 用の 画像ではこのふわっとした影が重要になる4ということなので、この欠点は致命的かもしれません。
Ruby から cairo を使うためには rcairo というライブラリを使う必要が あります。rcairo は単なる cairo のラッパーではなく、cairo の API 上 に Ruby で書かれた便利な機能を追加しています。
開発版の rcairo で実験的に「ぼかし」風の画像効果を実装していま
す。実験的というだけあってうまく動かない場合もあるのですが、
以下の実行例のようにアルファチャンネルがないものに対してはそ
れっぽく動きます。ただ、動作は少し重いです。
本稿では「ぼかし」風画像効果については扱いません。使いかたは rcairo のリポジトリにあるサンプルファイルを参考にしてください。
https://github.com/rcairo/rcairo/tree/master/samples
rcairo を使うと Ruby らしい API で cairo を使うことができる半面、以 下のような欠点があります。
導入に関しては、Windows5/一部の Linux/*BSD/Mac OS X6 にはパッケージが用意されているので、それを利用すれば負担を減 らすことができます。
rcairo 用の gem は RubyForge にアップロードされています。
ただし、gem にはバイナリは含まれていないので、gem のインストー ル時にビルドすることになります。そのため、gem を使ってもダウ ンロードする手間が減るくらいにしかならないかもしれません。
Ruby-GNOME2 プロジェクトのプロジェクトリーダであるむとうさん が Windows 版のバイナリを作成し、配布してくれています。このバ イナリをインストールすると Ruby-GNOME2 プロジェクトのライブラ リもインストールすることができるという特典付きです。
ドキュメント不足はこれ以降の文章で補ってください。
それでは、ここからは cairo を用いた基本的な画像の作り方を説明し ます。
Ruby で cairo を使うには rcairo を使います。rcairo は Ruby と cairo を つなぐ糊の役目を果たします。rcairo を使うことにより、より Ruby らしい方法で cairo を利用することができます。
1 つ例を挙げます。cairo には状態を保存する save と保存して おいた状態を復元する restore という API があります。これら は以下のようにペアで使います。
save
...
restore
save だけを呼んで restore を呼ぶことを忘れないため にブロックを使うこともできます。これはちょうど open のブ ロックの使いかたと同じです。
save do
...
end
rcairo で画像作成をする場合は以下のような流れになります。場合 によっては前処理や後処理の部分を省略することもできます7。
サーフェスは「表面」という意味で、出力先を抽象化したオブジェ クトです。cairo では PNG 用/PDF 用/SVG 用/GTK+ 用のサーフェスなど いくつものサーフェスがサポートされていますが、どれも Cairo::Surface のサブクラスになっています。このようにたくさん あるサーフェスですが、私たちはサーフェスに直接触れる機会はほ とんどありません。多くの操作はコンテキストに対して行います。
例えるなら、サーフェスがキャンバスでコンテキストは画家です。
りんごの絵が欲しいとします。まず、絵を描くためのキャンバスを
用意し、イーゼル8に掛けます (サーフェスの作成)。画家はキャンバスの前
に立ちます (サーフェス用のコンテキスト作成)。画家にりんごを
描いてくれるように頼みます (コンテキストに対する描画処理)。
描き終わったら画家にお礼をいい、りんごが描かれたキャンバスを
しまいます (サーフェスへ終了メッセージの送信)。
それでは、日の丸を描いてみましょう。出力結果はこうなります。
スクリプトは以下のようになります。
スクリプト: hinomaru.rb
require 'cairo'
format = Cairo::FORMAT_ARGB32
width = 300
height = 200
radius = height / 3 # 半径
surface = Cairo::ImageSurface.new(format, width, height)
context = Cairo::Context.new(surface)
# 背景
context.set_source_rgb(1, 1, 1) # 白
context.rectangle(0, 0, width, height)
context.fill
# 赤丸
context.set_source_rgb(1, 0, 0) # 赤
context.arc(width / 2, height / 2, radius, 0, 2 * Math::PI)
context.fill
surface.write_to_png("hinomaru.png")
以下の通り、順に説明します。
Cairo::ImageSurface はラスタ画像 (画素をたくさん集めて表現し た画像。ドット絵など。) を出力するためのサーフェスです。生の ラスタ画像を取り出すこともできますし、描いたラスタ画像を PNG 形式で出力することもできます。例では PNG 形式で出力しています。
Cairo::ImageSurface を作るには以下の 3 つの情報が必要です。
幅と高さが必要なのはわかると思います。問題は各画素が持つ情報 です。
各画素は RGB (Red, Green, Blue) で表現された色の情報やアルファ 値と呼ばれる画素がどのくらい透過するかという情報を持つことが できます。通常は色情報も透過情報もすべて使う Cairo::FORMAT_ARGB32 を指定すればよいでしょう。
幅 300、高さ 200 の画像を作成したい場合は、以下のように Cairo::ImageSurface を作ることになります。
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, 300, 200)
実は、1.4.0 以降の rcairo では Cairo::FORMAT_ARGB32 を省略して以下 のように書くこともできます。
surface = Cairo::ImageSurface.new(300, 200)
さらに、現在はまだリリースされていない 1.6.0 以降 では Cairo::FORMAT_ を省略して以下のように書くこともできます。
surface = Cairo::ImageSurface.new(:argb32, 300, 200)
他にも PNG 画像を読み込んで Cairo::ImageSurface を作る方法や、生 のラスタ画像データから作る方法もあります。詳しくはリファレン スマニュアルを参考にしてください。9
サーフェスを作ったら、そのサーフェスを操作するためのコンテキ ストを作ります。
コンテキストの作り方は簡単です。操作対象のサーフェスを渡すだ けです。
context = Cairo::Context.new(surface)
サーフェスへの操作はこのコンテキストを経由して行います。下準 備はこれでおしまいです。
ここからが本番です。基本的に描画は以下のような流れになります。
色は RGB 形式で set_source_rgb を使って設定します。RGB は 0.0-1.0 までの間で指定します。
context.set_source_rgb(1, 0, 0) # 赤
context.set_source_rgb(0, 1, 0) # 青
context.set_source_rgb(0, 0, 1) # 緑
context.set_source_rgb(0, 0, 0) # 黒
context.set_source_rgb(1, 1, 1) # 白
context.set_source_rgb(1, 1, 0) # 黄
context.set_source_rgb(0.9, 0.9, 0.9) # 薄いグレー
色だけではなく、アルファ値やグラデーションも設定できます。
色に関連するインターフェイスは、次のバージョンである rcairo 1.6.0 でかなり改善されます。まず、以下のように約 250 種類の有名 な色があらかじめ利用できます。
Cairo::Color::RED
Cairo::Color::WHITE
Cairo::Color::ROSE
...
これらはコンテキストの色指定として利用できます。
context.set_source_color(Cairo::Color::GREEN)
また、文字列を色に変換することもできます。
Cairo::Color.parse("red") # => Cairo::Color::RED
Cairo::Color.parse("#f00") # => Cairo::Color::RED
Cairo::Color.parse("#ff000033") # Cairo::Color::RED で
# アルファ値が 0.2
# (51/255, "33".hex == 51)
実は、コンテキストの色指定に上述の文字列指定も可能になります。
context.set_source_color("red") # == context.set_source_rgb(1, 0, 0)
他にも RGB だけではなく CMYK、HSV がサポートされます。もちろん、 RGB/CMYK/HSV 間での相互変換が可能です。
パスの操作にはパスを始める点を指定する move_to や現在の点
から直線を引く line_to などがあります。また、これらの基本的な
操作だけではなく、もう少し便利な四角形のパスを作る rectangle
や丸を作る arc などがあります。cairo では提供していませんが、
rcairo では角が丸まった四角形のパスを作る rounded_rectangle を
用意しています。図は rectangle と rounded_rectangle の使用例です。
日の丸の例では、まず、背景を真っ白に塗りつぶすために画像全体 と同じ大きさの四角形のパスを作っています。
context.rectangle(0, 0, width, height)
その後の赤い丸を描く処理では arc を使って丸いパスを作っていま す。
context.arc(width / 2, height / 2, radius, 0, 2 * Math::PI)
arc には円の中心となる点を指定します。赤い丸を画像の中央に描 くためには縦横それぞれで真ん中の点を指定する必要があります。 width / 2 と height / 2 はそのための計算です。
arc には中央の点だけではなく、半径と円をどの角度からどの角度ま で描くかということも指定します。半径は英語では radius です。問 題は角度の指定方法です。
角度はラジアンで指定します。0-360°で一周を表す度数法 (Wikipedia にそう書いてあった) ではありません。ラジアンでは 0-2πで一周になります。度数からラジアンへは以下のように変換 できます。
日の丸の赤い丸はどこも欠けていないので、一周分 (0 から 2 * Math::PI) の円を描いています。
context.arc(..., 0, 2 * Math::PI)
8 等分されたうち 1 切れ食べられたホールケーキを描くときは以下の ようになります。
context.arc(..., 0, (2 * Math::PI) * (7.0 / 8.0))
実は、今回の日の丸のようにどこも欠けていない円を描くために circle を使えます。
context.circle(width / 2, height / 2, radius)
これは以下のように書くことと同じです。
context.arc(width / 2, height / 2, radius, 0, 2 * Math::PI)
この完全な円を描くための便利なメソッド circle は cairo の API には ない、rcairo が拡張した API です。
パスを作っても描かなければ出力には何も現れません。パスを描く ためには以下のふたつの操作があります。
fill はパスの内側を塗りつぶす操作で、stroke はパスに沿って線を 描く操作です。今回は fill だけを使いました。
fill も stroke もパスを描くとパスを消してしまいます。通常はこ
の方が便利なのですが、パスが消されると困ることもあります。例
えば、赤い四角を描いて、その周りを黒く縁どりしたい場合です。
この場合は fill_preserve と stroke_preserve を使います。
context.set_source_rgb(1, 0, 0)
context.rectangle(50, 50, 100, 100)
# 赤い四角
context.fill_preserve
# 黒く縁どり
context.set_source_rgb(0, 0, 0)
context.stroke
より Ruby っぽく書けるように fill や stroke はブロックをとれます。
context.set_source_rgb(1, 0, 0)
context.rectangle(50, 50, 100, 100)
context.fill
この例は以下のようにも書けます。
context.fill do
context.set_source_rgb(1, 0, 0)
context.rectangle(50, 50, 100, 100)
end
この書き方の利点は break を使って途中で処理をやめることができ たり、fill の対象が明示的になるということです。
context.fill do
context.set_source_rgb(1, 0, 0)
break if error_occurs
context.rectangle(50, 50, 100, 100)
end
基本的な描画方法は以上の通りです。この他にもたくさん便利な操 作や高度な操作がありますが、描画の流れはここで紹介した以下の ようになります。
それでは描画した内容を出力してみましょう。
描画内容を出力する方法はサーフェスによって異なります。描画処 理では一切サーフェスを意識する必要はありませんでしたが、出力 のときは意識する必要があります。
Cairo::ImageSurface では write_to_png を使って PNG 形式で描画内容 を出力します。例では以下のようにサーフェス作成時に持っておい たサーフェスに対して write_to_png を呼び出していました。
surface.write_to_png("hinomaru.png")
サーフェスはコンテキストから取り出すことができるので以下のよ うに書くこともできます。
context.target.write_to_png("hinomaru.png")
Cairo::ImageSurface 以外のサーフェスでは finish を呼ぶことで出力 をするものが多いです。例えば、PDF を生成するサーフェスなどがそ うです。10
surface = Cairo::PDFSurface.new(...)
context = Cairo::Context.new(surface)
...
context.target.finish
cairo を用いた画像の作成では、出力先を抽象化したサーフェスと サーフェスへの操作を担当するコンテキストを使いました。
サーフェスを意識するのは、最初にサーフェスを作成するときと、
最後にサーフェスから結果を取り出すときだけです。描画操作はコ
ンテキストに対して行い、サーフェスを意識する必要はありません。
描画操作でサーフェスを意識する必要がないということは、PNG 形 式や PDF 形式など異なる出力先でも同じ描画処理ルーチンを使いま わすことができるということです。これは cairo を用いた場合の大 きなメリットです。
本稿ではグラデーションなどのより高度の機能の使いかたは紹介し ません。その代わり、他のライブラリと連携して使用する方法を紹 介します。
cairo を出力モジュールとして使っているライブラリがいくつかあ ります。例えば、SVG 描画ライブラリの librsvg や PDF 描画ライブラ リの poppler などです。ここではそれらのライブラリを利用した例 を示します。
これらのライブラリと連携すると、cairo と各描画ライブラリは図の
ような関係になります。
まずは SVG 描画ライブラリである librsvg と連携する例です。cairo は SVG 形式で出力できるライブラリですが、librsvg は SVG 形式のデー タを読み込んで描画するライブラリです。
Ruby から librsvg を利用するためには Ruby/RSVG が必要です。 Ruby/RSVG は Ruby-GNOME2 プロジェクトに含まれています。
ここでは、svg2png.rb という SVG ファイルを読み込んで、PNG 形式に 変換するスクリプトを作ります。単に変換するだけではつまらない ので拡大・縮小をサポートします。
スクリプト: svg2png.rb
#!/usr/bin/env ruby
require "rsvg2"
if ARGV.size < 1
puts "usage: #{$0} input.svg [scale_ratio]"
exit(-1)
end
input, ratio = ARGV
ratio ||= 1.0
ratio = ratio.to_f
handle = RSVG::Handle.new_from_file(input)
dim = handle.dimensions
width = dim.width * ratio
height = dim.height * ratio
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, width, height)
context = Cairo::Context.new(surface)
context.scale(ratio, ratio)
context.render_rsvg_handle(handle)
surface.write_to_png(input.sub(/\.[^.]+$/i, '.png'))
使い方はこうなります。
例えば、元の SVG 画像の半分の大きさの PNG 画像にしたい場合は以下 のようにします。
Ruby/RSVG を使うには以下のように rsvg2 を require します。
require 'rsvg2'
もし、システムに rcairo がインストールされていれば自動で rcairo も読み込みます。そのため、require ‘cairo’は必要はありません。
librsvg では SVG を操作するためにハンドルというオブジェクトを使 います。ハンドルとは車に付いているそれと同じです。車に付いて いるハンドルは車を操作しますが、librsvg のハンドルは SVG を操作 します。SVG 画像を読み込むのもハンドルですし、読み込んだ SVG 画 像を描画するのもハンドルです。
ハンドルは Ruby/RSVG では RSVG::Handle オブジェクトになります。 今回はファイルから SVG 画像を読み込み、RSVG::Handle オブジェク トを作ります。
handle = RSVG::Handle.new_from_file(input)
これだけで SVG 画像を描画する準備が整います。あとは出力用のサー フェスと、そのサーフェスを操作するためのコンテキストを用意す るだけです。
今回は PNG 画像として出力するため、サーフェスには Cairo::ImageSurface を使います。Cairo::ImageSurface を作るには 画像の幅と高さが必要でした。これらは変換元の SVG 画像の幅と高 さ、それと拡大率がわかれば求めることができます。
読み込んだ SVG 画像のサイズはハンドルが知っています。 dimensions メソッドは幅や高さなどを RSVG::DimensionData オブジェ クトとして返します。
dim = handle.dimensions
width = dim.width * ratio
height = dim.height * ratio
今回は幅と高さだけが必要なので、以下のように to_a で配列化し、 幅と高さだけを取り出すという方法もあります 11。
width, height, = handle.dimensions.to_a
width *= ratio
height *= ratio
幅と高さがわかったのでサーフェスを作ります。
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, width, height)
ハンドルが持っている SVG 画像をサーフェスに描画するためにコン テキストを作ります。
context = Cairo::Context.new(surface)
render_rsvg_handle でコンテキストにハンドルを描画します 12。
context.render_rsvg_handle(handle)
しかし、このままだと拡大率が反映されず、元の SVG 画像と同じ大き さで描画されてしまいます。拡大率を変更するには scale を使います。
context.scale(ratio, ratio)
今回は縦も横も同じ拡大率を使うため、ratio が 2 つ並んでいます。
scale で拡大率を設定すると、それ以降は設定した拡大率で描画され ます。今回は拡大率に合わせて SVG 画像を描画したいので render_rsvg_handle の前に scale を呼ぶ必要があります。
context.scale(ratio, ratio)
context.render_rsvg_handle(handle)
これでサーフェスに SVG 画像と同じ内容が設定した拡大率で描画さ れます。あとは PNG 画像を出力するだけです。
surface.write_to_png(input.sub(/\.[^.]+$/i, '.png'))
以上で SVG 画像を PNG 画像に変換できます。
次は画像読み込み・操作ライブラリである GdkPixbuf と連携する例で す。GdkPixbuf は GTK+ に含まれていますが、X Window System など GUI 環境がなくても動作します。つまり、サーバ上で CGI のバックエ ンドとして利用することもできるということです。
Ruby から GdkPixbuf を利用するためには Ruby/GdkPixbuf2 が必要で す。Ruby/GdkPixbuf2 は Ruby-GNOME2 プロジェクトに含まれています。
rcairo と連携する例の前に、まず Ruby/GdkPixbuf2 の例を示します。
GdkPixbuf は以下の 3 つの機能を提供します。
以下は PNG 形式の画像を 180°回転させ、JPEG 形式として保存する例で す。
スクリプト: png-to-inverted-jpeg.rb
require 'gdk_pixbuf2'
input = ARGV.shift
output = input.sub(/\.png$/i, ".jpg")
pixbuf = Gdk::Pixbuf.new(input)
inverted_pixbuf = pixbuf.flip(false)
inverted_pixbuf.save(output, "jpeg")
GdkPixbuf は画像形式を自動認識できます。そのため、任意の形式 の画像を PNG 形式に変換するスクリプトは以下のようになります。
スクリプト: image2png.rb
require 'gdk_pixbuf2'
input = ARGV.shift
output = input.sub(/\.[^.]+$/i, ".png")
Gdk::Pixbuf.new(input).save(output, "png")
GdkPixbuf がサポートしている画像形式は PNG や JPEG、GIF など 10 数種 類あります。GdkPixbuf の画像入出力モジュールは後から追加でイン ストールすることができ、前述の librsvg をインストールしてあれば SVG 形式も読み込むことができます。
それでは、rcairo と Ruby/GdkPixbuf2 が連携する例として、画像を PDF 形式に変換するスクリプトを作ります。PDF 形式で出力するので、 サーフェスには今まで使ってきた Cairo::ImageSuface ではなく Cairo::PDFSurface を使います。
Cairo::PDFSurface を使うときは、show_page を呼ぶことを忘れない ようにしてください。これを忘れるとファイルには何も出力されま せん。
show_page はページという概念のないサーフェスでは省略できます。 例えば、今まで利用してきた 1 枚の画像を表している Cairo::ImageSurface がページという概念のないサーフェスです。実 際、今までの例では show_page を省略してきました。しかし、PDF 形 式のようにページという概念があるサーフェスは show_page のタイミ ングで描画内容の書き出しを行うので、省略すると望んだ結果を得 られません。Cairo::PDFSurface 以外では PostScript を出力する Cairo::PSSurface と SVG を出力する Cairo::SVGSurface が show_page を省略できません。
以下が画像を PDF 形式に変換するスクリプトです。
スクリプト: image2pdf.rb
require 'gdk_pixbuf2'
require 'cairo'
input = ARGV.shift
output = input.sub(/\.[^.]+$/i, ".pdf")
pixbuf = Gdk::Pixbuf.new(input)
width = pixbuf.width
height = pixbuf.height
surface = Cairo::PDFSurface.new(output, width, height)
context = Cairo::Context.new(surface)
context.set_source_pixbuf(pixbuf)
context.paint
context.show_page
surface.finish
このスクリプトでるびまのロゴを変換すると以下のようになります。
それでは、Cairo::PDFSurface オブジェクトの作成と Gdk::Pixbuf オ ブジェクトの描画について説明します。
スクリプトの説明に入る前に、残念なお知らせがあります。このス クリプトは Ruby/GdkPixbuf2 0.16.0 より前のバージョンでは動きま せん。これより前のバージョンでも動くようにするには以下のよう にします。
require 'gtk2' # 追加
context.set_source_pixbuf(pixbuf) # 変更前
context.set_source_pixbuf(pixbuf, 0, 0) # 変更後
ただし、このスクリプトは X Window System など GUI 環境がないと動 作しません。ただし、どうやっても動作しないというわけではなく て、ちょっとした小細工をすると動作します。問題は以下の部分で す。
require 'gtk2'
0.16.0 より前のバージョンでは rcairo と Ruby/GdkPixbuf2 を連携させ る部分は Ruby/GTK2 に含まれています。そして、最近の Ruby/GTK2 は require すると GUI 環境の初期化も行ってくれます。そのため、GUI 環 境がない場合は require しただけで例外が発生してしまいます。
rcairo と Ruby/GdkPixbuf2 の連携部分は GUI 環境がなくても動作しま す。つまり、GUI 環境の初期化が失敗したという例外は無視してもよ いのです。そこで、以下のようにします。
begin
require 'gtk2'
rescue RuntimeError
end
これで GUI 環境がなくても動作します。
それでは本題に入ります。
PDF 形式用のサーフェスは Cairo::PDFSurface です。今回はこのサー フェスを使います。Cairo::PDFSurface オブジェクトを作るために は 3 つの情報が必要になります。
幅と高さはポイントで指定します。今回は画像と同じ大きさのペー ジを作成するので以下のようになります。
output = input.sub(/\.[^.]+$/i, ".pdf")
width = pixbuf.width
height = pixbuf.height
surface = Cairo::PDFSurface.new(output, width, height)
実は、ファイルではなく、ネットワーク上やメモリ上に出力するこ ともできます。そのときは出力ファイル名ではなく、write メソッド を持ったオブジェクトを指定します。以下はメモリ上に PDF を出力 する例です。
require 'stringio'
output = StringIO.new("")
surface = Cairo::PDFSurface.new(output, width, height)
...
pdf = output.string
StringIO オブジェクトの代わりに Socket オブジェクトを指定すれば、 ネットワーク上に出力することができますし、$stdout を指定すれ ば標準出力に出力することもできます。
Gdk::Pixbuf をコンテキストに描画する方法を説明する前に、コン テキストのソースについて説明しなければいけません。
ソースはカーボン紙のようなものです。パスを描いたり塗りつぶし
たりすると、ソースの内容がソースの下にあるサーフェスに描画さ
れます。
しかし、普通のカーボン紙とは違って単色とは限りません。グラデー
ションやサーフェスに描画した内容を使うことも出来ます。
コンテキストはいつも必ず 1 つソースを持っています。今までは set_source_rgb で色を指定し、ソースを設定していました。この方 法だとパスは 1 色で単調に描かれます (または塗りつぶされます)。
画像を描画するには、画像の内容をソースに設定しパスを塗りつぶ します。もう少し細かくいうと、以下のような流れになります。
コードにするとこうなります。
width = pixbuf.width
height = pixbuf.height
context.set_source_pixbuf(pixbuf)
context.rectangle(0, 0, width, height)
context.fill
実は、画像の描画には rectangle と fill の組合せよりも paint の方が 便利です。paint を使うとこうなります。
context.set_source_pixbuf(pixbuf)
context.paint
paint は現在設定されているソースでサーフェス全体を塗りつぶす 操作です。そのため、rectangle で行っていたパスを作るという操 作が不要になります。
paint の利点はパスを作らなくてもよいというだけではありません。 アルファ値を設定することもできます。例えば、以下のようにする と画像全体が半透明になって描画されます。
context.set_source_pixbuf(pixbuf)
context.paint(0.5)
最初に image2pdf.rb のソースを紹介したときも触れましたが、忘れ てハマリやすいことなのでもう一度触れておきます。
今回使った Cairo::PDFSurface や SVG を出力するための Cairo::SVGSurface などページという概念があるサーフェスでは、各 ページの描画が終わった後に show_page を呼ぶことを忘れないでくだ さい。忘れると描画内容が正しく書き出されず、出力内容が壊れて しまいます。
また、全ての描画が終わった後に finish を呼ぶことも忘れないでく ださい。show_page と同様に出力内容が壊れてしまったり、出力され たはずと思っていたのにまだ出力されていなかった、ということが あります。
現在の安定版 cairo 1.4.x では finish に関してはサーフェスが GC され るときに内部で自動的に呼び出されるため省略可能と言えば省略可 能です。ただし、どのタイミングでサーフェスが GC されるかはいつ でも同じとは限らないため、GC に期待してスクリプトを書くことは 危険です。これはちょうど File オブジェクトが GC されるときに close されるという関係と似ています。
次の rcairo のリリース (おそらく 1.6.0) では File.open の ように以下のように書けるようになります。
Cairo::ImageSurface.new(300, 300) do |surface|
context = Cairo::Context.new(surface)
...
end
この書き方では File.open と同じように、ブロックを抜けるときに 自動的に finish されます。
次は画像の描画方法をもう少し詳しく説明するための例です。前の
例と似ていますが少しだけ違います。今度の例も画像を PDF に変換
しますが、今度は画像に余白を付けて PDF に変換します。
この例では左隅に画像を配置するのではなく、任意の場所に配置す る方法を示します。実際のスクリプトは以下のようになります。
スクリプト: image2pdf-with-margin.rb
require 'gdk_pixbuf2'
require 'cairo'
input, margin = ARGV
output = input.sub(/\.[^.]+$/i, ".pdf")
margin ||= 10
margin = Integer(margin)
pixbuf = Gdk::Pixbuf.new(input)
width = pixbuf.width + 2 * margin
height = pixbuf.height + 2 * margin
surface = Cairo::PDFSurface.new(output, width, height)
context = Cairo::Context.new(surface)
context.translate(margin, margin)
context.set_source_pixbuf(pixbuf)
context.paint
context.show_page
surface.finish
画像の位置を左隅ではなく任意の位置に平行移動させてソースに設 定する場合は translate を使います。もうすっかり忘れてい ると思いますが、translate はパスを平行移動させるときに も使っていました。
context.translate(margin, margin)
context.set_source_pixbuf(pixbuf)
context.paint
大事なことは set_source_pixbuf を呼ぶ前に translate
を呼ぶことです。位置をずらして描画したいのだから描画操作であ
る paint を呼ぶ前でもよさそうですが、それではうまくいきま
せん。それはパスではなく、ソースをずらす必要があるからです。
一方、画像の位置はそのままで、画像の一部だけを描画したい場合
はパスをずらします。
図では paint ではなく rectangle を使っていますが、これは paint は translate の影響を受けないからです。paint は無限大に広がる rectangle を指定してから fill していると考えてください。つまり、 paint の前に translate をしても translate の対象としたい rectangle が大きすぎて、translate で多少動かしたとしても全く影響を与えら れないのです。
次は PDF 描画ライブラリである poppler と連携する例です。cairo は PDF 形式で出力できるライブラリですが、poppler は PDF 形式のデータ を読み込んで描画するライブラリです。
Ruby から poppler を利用するためには Ruby/Poppler が必要です。 Ruby/Poppler は Ruby-GNOME2 プロジェクトに含まれています。
ここでは、pdf-thumbnail.rb というスクリプトを作ります。このス クリプトは PDF ファイルを読み込んで、各ページを縮小し、一覧表示 した PDF を生成します。この例も X Window System など GUI 環境なしで 動きます。
注: ただし、6 月にリリースされるはずの poppler 0.6.0 が必要です。
以下に、入力 PDF と実行結果の PDF を示します。
スクリプト: pdf-thumbnail.rb
#!/usr/bin/env ruby
require "poppler"
if ARGV.size < 1
puts "usage: #{$0} input.pdf"
exit(-1)
end
input = ARGV.shift
output = input.sub(/(\.[^.]+)$/, '-thumb\1')
columns = 3
rows = 2
pages_in_a_page = rows * columns
x_ratio = 1.0 / columns
y_ratio = 1.0 / rows
input_uri = GLib.filename_to_uri(File.expand_path(input))
document = Poppler::Document.new(input_uri)
first_page = document[0]
width, height = first_page.size
surface = Cairo::PDFSurface.new(output, width, height)
context = Cairo::Context.new(surface)
width_per_page = width / columns
height_per_page = height / rows
need_show_page = true
document.each_with_index do |page, i|
row, column = i.divmod(columns)
row = row.modulo(rows)
x = width_per_page * column
y = height_per_page * row
context.save do
context.translate(x, y)
context.scale(x_ratio, y_ratio)
context.render_poppler_page(page)
context.set_source_rgb(0.2, 0.2, 0.2)
context.rectangle(0, 0, width, height)
context.stroke
end
need_show_page = ((i + 1) % pages_in_a_page).zero?
context.show_page if need_show_page
end
context.show_page unless need_show_page
surface.finish
スクリプトが長くなって非常に心苦しいです。
使い方はこうです。
生成される PDF ファイルの名前は元の PDF ファイル名に -thumb がつい たものになります。例えば、以下のように slide.pdf というファイル を指定した場合は slide-thumb.pdf というファイルができます。
Ruby/Poppler を使うには以下のように poppler を require します。
require "poppler"
もし、システムに rcairo がインストールされていれば自動で rcairo も読み込みます。そのため、cairo を require する必要はありません。 13
poppler では PDF 文書がドキュメントオブジェクトに対応します。ド キュメントオブジェクトを作るには PDF 文書の URI を渡します。PDF 文書のファイル名ではなくて URI です。例えば、/tmp/xxx.pdf とい う PDF 文書を開きたい場合は file:///tmp/xxx.pdf と指定する必要が あります。
これを行うための慣用句が以下になります。
input_uri = GLib.filename_to_uri(File.expand_path(input))
document = Poppler::Document.new(input_uri)
しかし、これではさすがに使いづらいので、次のリリースから (お そらく Ruby-GNOME2 0.17) は以下のように書けます。
document = Poppler::Document.new(input)
つまり、URI ではなく直接ファイル名を指定できるようになります。
今回は PDF を生成するので、サーフェスは Cairo::PDFSurface を使い ます。Cairo::PDFSurface オブジェクトを作るために必要な情報は以 下の 3 つでした。
出力ファイル名は入力ファイル名に -thumb を付けることにしました。
input = ARGV.shift
output = input.sub(/(\.[^.]+)$/, '-thumb\1')
ページの幅と高さは元の PDF 文書の最初のページの幅と高さと同じに します。
ドキュメントオブジェクトから各ページへは配列のようにアクセス できます。例えば、3 ページには以下のようにアクセスできます。
third_page = document[2]
各ページを順番にアクセスしたい場合は each が使えます。
document.each do |page|
...
end
本当に配列としてアクセスしたい場合は pages を使います。
p document.pages # [#<Poppler::Page:...>, ...]
ページの幅と高さは size で取り出せます。
first_page = document[0]
width, height = first_page.size
これで最初のページの幅と高さを取得することが出来るようになり ました。以下のように Cairo::PDFSurface を作ることが出来ます。
first_page = document[0]
width, height = first_page.size
surface = Cairo::PDFSurface.new(output, width, height)
context = Cairo::Context.new(surface)
あとは、各ページを縮小して描画するだけです。ただし、1 ページに
縮小された複数のページを描画するため、各ページの描画位置を計
算し、並んで描画するようにしなければいけません。
描画位置を計算する部分は本質的ではないため、ここでは省略しま す。このスクリプトの本質的な部分は以下のようになります。
x_ratio と y_ratio はそれぞれ x 方向、y 方向の拡大率です。図では横 に 3 ページ、縦に 2 ページ表示しているので、x 方向 (x_ratio) は 1/3 倍、y 方向 (y_ratio) は 1/2 倍の拡大率になります。
need_show_page は 1 ページに表示する最後の縮小ページを描画した ときに真になります。例えば、図だと 1 ページには 6 ページ分の縮小 ページを描画するので、6 ページ毎に真になります。
need_show_page = true
document.each_with_index do |page, i|
...
context.save do
context.translate(x, y)
context.scale(x_ratio, y_ratio)
context.render_poppler_page(page)
...
end
...
context.show_page if need_show_page
end
context.show_page unless need_show_page
描画する x/y 座標の変更を translate で行うのは image2pdf-with-margin.rb の例と同じです。元の描画対象を縮小さ せるために scale を使うのは svg2png.rb の例と同じです。
上記のコードで省略した以下の部分は縮小したページに枠を描画し ています。
document.each_with_index do |page, i|
...
context.save do
...
context.set_source_rgb(0.2, 0.2, 0.2)
context.rectangle(0, 0, width, height)
context.stroke
end
...
end
rectangle が
context.rectangle(x, y, width_per_page, height_per_page)
ではなく、
context.rectangle(0, 0, width, height)
となっているのはその前にある
context.translate(x, y)
context.scale(x_ratio, y_ratio)
の影響を受けるからです。
この例を動かすには 6 月にリリースされる予定の poppler 0.6.0 が必 要です。また、画像を含む PDF 文書を処理すると非常に重いです (これは cairo の問題)。
次はテキスト描画ライブラリである Pango と連携する例です。cairo 単体でもテキストを描画できますが、Pango と組み合わせることで、 より高度に描画することができます。例えば、センタリングや、一 部だけ文字の大きさを変えたり、下線を引いたりできます。
この例では入力されたファイルの中のテキストを A4 用紙に描画する PostScript ファイルに変換します。この例も X Window System など GUI 環境なしで動きます。
スクリプト: text2ps.rb
require 'pango'
input = ARGV.shift
output = input.sub(/\.[^.]+$/i, ".ps")
width = 595
height = 842
surface = Cairo::PSSurface.new(output, width, height)
context = Cairo::Context.new(surface)
layout = context.create_pango_layout
layout.text = File.read(input)
context.show_pango_layout(layout)
context.show_page
surface.finish
このスクリプトにスクリプト自身を与えると以下のようになります。 text2ps-to-text2ps.ps
Ruby/Pango を使うには以下のように pango を require します。
require "pango"
もし、システムに rcairo がインストールされていれば自動で rcairo も読み込みます。そのため、cairo を require する必要はありません。 14
Pango ではレイアウト (Pango::Layout) がテキストの描画のしかた を計算します。例えば、描画開始位置は中央揃えにしているか、右 揃えにしているかなどで変わります。1 行に入る文字数は、行の幅、 あるいはフォントやフォントの大きさなどで変わります。レイアウ トは与えられた整列方法やフォントから、描画開始位置や 1 行の文字 数などのような具体的な描画のしかたを計算します。
Pango を cairo と一緒に使う場合は、コンテキストからレイアウトを 作ります。
layout = context.create_pango_layout
単にテキストをデフォルトの設定でそのまま描画するだけなら、こ れで十分です。
layout.text = text
context.show_pango_layout(layout)
用紙サイズで折り返したいなら幅を指定します。注意しなければい けないことは、幅は Pango の世界の単位で指定するということです。 ピクセルを Pango の世界の単位に変換するには Pango::SCALE を掛けます。
layout.width = width * Pango::SCALE
折り返しの区切りは単語単位か文字単位になります。日本語ではど ちらも同じように扱われますが、英語では以下のように異なります。
単語単位:
| layout.width |
|<------------->|
long long long
long long long
long
文字単位:
| layout.width |
|<------------->|
long long long lo
ng long long long
Pango での指定のしかたはそれぞれ以下のようになります。
layout.wrap = Pango::WRAP_WORD # 単語単位
layout.wrap = Pango::WRAP_CHAR # 文字単位
他にもレイアウトにはたくさんのオプションが設定できます。例え ば、以下のように入力テキストにマークアップをして背景色を設定 できたりします。
markup = "t<span background='green'>ex</span>t"
attr_list, text = Pango.parse_markup(markup)
layout = context.create_pango_layout
layout.text = text
layout.attributes = attr_list
context.show_pango_layout(layout)
実行すると図のように「ex」の部分の背景色だけが緑色になります。
最後に、今まで紹介したライブラリだけではなく、GTK+ とも連携す る例を示しておきます。PDF/SVG/PNG/JPEG などを表示するアプリケー ションです。ウィンドウサイズを変更すると、それにあわせて表示し ている内容も拡大・縮小します。
Ruby/GTK2 に関してはここでは詳しく説明しませんが、GTK+ で描画す るためには以下のようにすればよいということだけわかれば読める と思います。
drawing_area.signal_connect("expose-event") do |widget, event|
context = widget.window.create_cairo_context
# context を使って描画
end
スクリプト: image-viewer.rb
#!/usr/bin/env ruby
require 'gtk2'
require 'rsvg2'
require 'poppler'
if ARGV.size != 1
puts "Usage: #{$0} IMAGE"
exit 1
end
input = ARGV.first
case input
when /\.pdf/i
document = Poppler::Document.new(input)
size = Proc.new do
document[0].size
end
draw = Proc.new do |context|
context.render_poppler_page(document[0])
end
when /\.svg/i
handle = RSVG::Handle.new_from_file(input)
size = Proc.new do
dim = handle.dimensions
[dim.width, dim.height]
end
draw = Proc.new do |context|
context.render_rsvg_handle(handle)
end
else
pixbuf = Gdk::Pixbuf.new(input)
size = Proc.new do
[pixbuf.width, pixbuf.height]
end
draw = Proc.new do |context|
context.set_source_pixbuf(pixbuf)
context.paint
end
end
window = Gtk::Window.new
window.set_default_size(*size.call)
window.signal_connect("destroy") do
Gtk.main_quit
false
end
drawing_area = Gtk::DrawingArea.new
drawing_area.signal_connect("expose-event") do |widget, event|
context = widget.window.create_cairo_context
x, y, w, h = widget.allocation.to_a
context.save do
image_width, image_height = size.call
context.scale(w / image_width.to_f, h / image_height.to_f)
draw.call(context)
end
true
end
window.add(drawing_area)
window.show_all
Gtk.main
cairo は PNG や PDF など複数の出力をサポートするだけではなく、 Windows/Linux/Mac OS X など多くの環境で動作し、その環境上の GUI へ描画することもできます。このため、cairo を標準的な描画 API としてとらえ、採用するプロジェクトやライブラリが増えてき ています。本稿で紹介した GTK+ や librsvg、poppler などもそのひと つです。
これからも cairo に対応したライブラリは増えていくことでしょう。 みなさんのプログラムでも描画 API として cairo を採用してみてはい かがでしょうか。
本稿では、まず rcairo の基本的な使いかたを紹介しました。次に、 cairo をサポートしている周辺ライブラリなどと連携する例を紹介し ました。これらのライブラリを使用して、SVG、画像ファイル、PDF 文書、素のテキストなどを cairo を使って描画しました。本稿では グラデーションなど rcairo 単体で行えるより高度な機能については 紹介していません。
また、以下の cairo の利点を紹介しました。
一方、欠点は以下の 2 点です。
パッケージがある環境では簡単に導入できる場合もありますが、レ ンタルサーバなどを利用している場合は難しいかもしれません。
画像効果のサポートはあまり期待できないでしょう。cairo のメー リングリストで、「ぼかし」を使うには?、という質問が出ても誰 も返信しないのが現状です。
他のライブラリと連携して解決することもできます。例えば、画像 を PNG で出力し、その結果を RMagick で加工する、などです。
cairo は Firefox や GTK+ などの有名プロジェクトのバックエンドとし て採用されている将来性のあるプロジェクトです。次のリリースで は、Mac OS X の Quartz も正式なサポートに加わる予定です。EPS の出 力もサポートも予定されています。サーフェスを切り替えるだけで、 同じプログラムがますますいろいろな環境で動き、いろいろな出力 をサポートできるようになるでしょう。
最後に、本稿で触れていない cairo の使いかたを知るために参考にな る URL を紹介します。
英語でしかも Python なのですが、cairo の世界についてわかりやすく書かれています。
英語で書かれている cairo のドキュメントです。本稿で触れていない多くのことが書かれています。
須藤功平。すどうではなく、すとう。東京に出ていった親戚はすどうになった。
得意分野は一般受けしないライブラリ・アプリケーション開発。最近がんばっているのは rcairo や ActiveLdap。どちらも他の人が始めた、当時は死にかけていたプロジェクト。だいぶ息を吹き返してきたと思っているが、知っている人はほとんどいないはず。
あ、そうか。あんまりドキュメントを書かないからか。
UNIX 環境でもっともよく使われている GUI ツールキットの 1 つ ↩
便利のために上述の draw_line のようなメソッドを作ればよいのかもしれませんが、そのようなメソッドは提供されていません。提供した方がよいでしょうか。 ↩
+ の方向が右で、-の方向が左です。 ↩
Ruby-GNOME2 プロジェクトが公開しているバイナリ群に rcairo のバイナリも含まれている。 ↩
MacPorts 用の Portfile: http://www.cozmixng.org/repos/dports/trunk/ruby/rb-rcairo/Portfile ↩
PNG 画像を作成するときは 4.を省略できます。GTK+ とともに使う場合は 1.と 4.を省略できます。 ↩
キャンバスを載せる台のことをそういうみたいです。 ↩
cairo 1.4.x からは finish は省略可能になりました。 ↩
RSVG::DimensionData#to_ary を定義してもよいかもしれません。よし、定義しよう。した。 ↩
Ruby/RSVG が rcairo をサポートしていなければ Cairo::Context#render_rsvg_handle は定義されません。Ruby/RSVG が rcairo をサポートしているかは RSVG.cairo_available?で判断できます。 ↩
Ruby/Poppler が rcairo をサポートしているかは Poppler.cairo_available? で判断できます。 ↩
Ruby/Pango が rcairo をサポートしているかは Pango.cairo_available? で判断できます。 ↩