こんにちは。開発チームのハッタです。
前職が業務系SIerだったこともあり、主にバックオフィス系のシステムを担当しています。
好きな言語はVB.NETとPL/SQLです。

業務系システムといえば必ず出てくるのが帳票類です。
.NETならGUIでサクサク作れるのですが、Webサービスメインの会社では雰囲気的にそうもいかないことが多いです。
(自分ひとりで保守を続けることになったり、、)

そこで、郷に入っては郷に従えの精神でWeb系言語による帳票開発をやってみました。

今回は、言語はRuby、ライブラリはPrawnを使ってPDFを作成します。
Prawnはググった限りでは数年前からあるようなのですが、日本語の情報があまりありません。
この記事が、リファレンスとしてみなさんのお役に立てれば幸いです。

導入
gemでインストールし、requireするだけです。

帳票レイアウトを決める
帳票のサイズや向き、上下左右の余白はコンストラクタで指定します。
  Prawn::Document.new(:page_size => 'A4',
                      :page_layout => :portrait,
                      :top_margin => 40,
                      :bottom_margin => 30,
                      :left_margin => 20,
                      :right_margin => 10)
または、
  Prawn::Document.generate("test.pdf",
                          :page_size => 'A4',
                          :page_layout => :portrait,
                          :top_margin => 40,
                          :bottom_margin => 30,
                          :left_margin => 20,
                          :right_margin => 10)
という書き方をします。
generate()はオブジェクトの生成と、指定したファイルパスへの保存を行います。

オプションの説明:
  :page_size      帳票のサイズです。'A4'、'B5'といった規格の他、[100, 200]という指定もできます。[横, 縦]、単位はポイント(pt)です。
  :page_layout    帳票の向きを縦 or 横で指定します。縦 = :portrait、横 = :landscape です。
  :top_margin     上余白をptで指定します。
  :bottom_margin  下余白をptで指定します。
  :left_margin    左余白をptで指定します。
  :right_margin   右余白をptで指定します。

上記のコードで作られるPDFの余白と印刷領域を塗り分けると、以下のようなイメージになります。
01_margin

































帳票のレイアウトを決めたら、文字や線を描いていきます。
それらの処理は以下のようにblockで渡す形で書きます。
  Prawn::Document.new(...) do |pdf|
    pdf.foo ...
  end

座標を把握する
PrawnではX座標、Y座標をptで指定して、文字や線の出力位置を決めます。

Xの値は、印刷領域の左端が起点です。
印刷領域の右端は、
 pdf.bounds.right
で取得できます。
Yの値は、印刷領域の下端が起点です。
印刷領域の上端は、
 pdf.bounds.top
で取得できます。

また、
 pdf.y
 pdf.cursor
で、現在のポインタのY座標が取得できます。
オブジェクト生成時の初期値は、印刷領域の上端 = pdf.bounds.topと同値が設定されています。

この値は、後述する文字出力などの処理を行うと、下方向へ移動していきます。(数値が減少)
y:bottom_marginを考慮せず帳票下端からの位置、
cursorは:bottom_marginを含めた下端からの位置になります。

以下のようなコードで、ポインタのY座標を動かすことができます。
  pdf.move_up 100           #現在位置から100pt上へ移動
  pdf.move_down 100         #現在位置から100pt下へ移動
  pdf.move_cursor_to(100)   #帳票下端から(100 + :bottom_margin)pt上の位置へ移動
上下左右に100ptの余白を設定したA4横帳票で、
座標移動処理を実行した後のyおよびcursorの位置は以下の図のようになります。
(枠線は印刷領域と余白の境界を表しています。)

02_point

















ページヘッダ、フッタを出力するには
ページヘッダ、フッタの出力など、毎ページ同じ処理を行いたい場合はrepeat()を使います。
  pdf.repeat :all do
    ...
  end
block内に文字や線の出力など、各種処理を書きます。
上記の例では、:allで全ページ対象、他に:oddで奇数ページ、:evenで偶数ページといった指定もできます。

後述するグリッド処理(table())で改ページが発生すると、
次のページは印刷領域の上端からグリッドの印字が始まるため、
repeat()内で行う出力に印刷領域内の座標を指定すると、
グリッドと重なってしまいます。
その場合は、座標にpdf.bounds.topを超える値を指定して、
上余白部分へ印字することで対応できます。

フォントを指定する
まず、文字を出力する際に必要となるのがフォントです。
Prawnでは何も指定しない場合、Helveticaが使われます。

日本語出力をしたい場合など、別のフォントを使いたい場合は以下のように書きます。
  pdf.font "/foo/bar.ttf", :size => 10
指定するファイルは.ttfファイルです。サイズの指定は省略できます。デフォルトでは12ptになります。
フォントファイルを指定後、サイズのみ変更したい場合は
  pdf.font_size = 10  #ptで指定
と書きます。

文字を出力する際、縦方向に必要となる長さは
  pdf.height_of(string)
で取得できます。
横方向は
  pdf.font.compute_width_of(string)
で取得できます。
これらを使うと、下図のように文字列と同じ長さの線を引くことなどが容易になります。
03_font










文字を出力する
Prawnには文字を出力するメソッドが3つあります。

1. text
  pdf.text 'foo'
  [0, pdf.cursor]の位置に指定された文字を出力します。
  文字の上端が指定されたY座標に当たる形で出力されます。
  このメソッドを実行すると、文字列1行分(= height_of(text))、Y座標が下に移動します。

2. draw_text
  pdf.draw_text 'foo', :at => [100, 200]
  :atに指定された座標に文字を出力します。
  文字の下端が指定されたY座標に当たる形で出力されます。
  このメソッドは、Y座標の移動をしません。

3. text_box
  pdf.text_box "foo", :at => [100, 200], :width => 200, :height => 100, :align => :center, :valign => :center
  :atに指定された座標を起点として、:width:heightで指定された枠内に文字を出力します。
  文字列が1行に収まらない場合、:heightの範囲内で折り返して出力されます。
  枠内に収まらない部分は切り捨てられ、戻り値として返します。
  文字列に半角スペースがある場合、それを区切り文字として単語ごとに分解されるため、想定外の箇所で折り返されることがあります。
  その場合は出力する文字列の半角スペースを以下のように置き換えることで、単語分解されずに出力されます。
    string.gsub(" ", Prawn::Text::NBSP)
  :alignは横の位置、:valignは縦の位置を指定できます。
  帳票タイトルを印字する場合に、:widthに印刷領域の最大幅を指定し、:align => :centerをするなどの活用方法があります。

  文字の上端が指定されたY座標に当たる形で出力されます。
  このメソッドは、Y座標の移動をしません。

各メソッドの座標の動きや、text_box()の折り返しをまとめると、以下の図のようになります。
04_text


















線を引く
線の始点と終点を指定するline()、線を描画するstroke()を実行します。
  pdf.line([0, 0], [0, 100])      #左辺
  pdf.line([0, 0], [100, 0])      #下辺
  pdf.line([100, 0], [100, 100])  #右辺
  pdf.line([0, 100], [100, 100])  #上辺
  pdf.stroke                      #描画
上記のコードで、帳票左下に一辺100ptの正方形が描画されます。

線の太さや色の変更、破線の出力もできます。
  pdf.line_width = 2          #太さ
  pdf.dash = 2                #破線間隔
  pdf.stroke_color("FF0000")  #色
太さ、破線はpt、色はRGB16進数で指定します。
太さ等はstroke()が実行される直前に指定された値が採用されます。
太さ、破線間隔ともに指定する数値の単位はptです。

破線を実線に戻す場合は、
  pdf.undash
と書きます。

line_width = 1..3dash = 1..3 をそれぞれ出力すると、以下のようになります。
06_line



















複数の出力項目をグルーピングする

bounding_box()というメソッドを使って枠を定義し、
その中の相対的な座標位置で文字出力などを行うことができます。

たとえば、
  pdf.bounding_box([0, 100], :width => 160, :height => 90) do
    pdf.draw_text "★", :at => [0, 0]
  end
上記のコードでは、
"★"は帳票全体の左下ではなく、
帳票下端から100pt上の位置に置かれた高さ:90pt、幅:160ptの枠の左下に出力されます。
block内でpdf.ypdf.cursorを実行した場合も、枠内の相対的な座標を取得します。

bounding_box()に渡す座標位置を変えることで、
枠内の文字や線の位置関係は変えずに、帳票全体の中での出力位置を変えることができます。
たとえば、
差出人の情報と宛先の情報がある帳票で、
それぞれの住所・氏名等をそれぞれのbounding_box内でまとめておけば、
位置の入替などが容易に行えます。

block内で
  pdf.stroke_bounds
と書くことで、枠線を出力することもできます。
2つbounding_boxを作成し、その中に同じ座標を指定した文字出力処理を書いた場合、
以下のように親のbounding_boxに依存して出力位置が変わります。
07_bounding_box


















データグリッドを出力する
今までにご紹介したtext()line()を駆使すれば、データグリッドを作成することは可能ですが、
Prawnにはtable()という簡単にグリッドを作成できるメソッドがあります。
  pdf.table(
    data,
    :column_widths => [70, 80, 90],
    :header => true,
    :row_colors => ["FF0000", "0000FF"]
  ) do |t|
    t.cells.border_width = 0.1
    t.columns(0).style :align => :left
    t.columns(1).style :align => :center
    t.columns(2).style :align => :right
    t.row(0).style :align => :center, :background_color => "CCCCCC"
  end
dataには二次元配列を渡します。
ヘッダ行を出力したい場合は、dataの先頭にヘッダ行の配列を格納します。

:column_widthsには各列の幅を指定します。
省略された場合は、最長データに合わせて幅が設定されます。
最長データよりも短い幅を指定した場合は、折り返して出力します。

:headerにはdata[0]の値をヘッダ行として扱うかを指定します。
trueが指定された場合は、上記のコード例でblockに書いた
  t.row(0).style :align => :center, :background_color => "CCCCCC"
などのような、ヘッダ行個別の設定が有効になります。
falseまたは指定なしの場合、ヘッダ行個別の設定は効きませんが、data[0]の値自体は明細行の先頭として出力されます。

:row_colorsには明細行の色を指定します。
明細行が交互に配列で指定された色で塗られます。
配列には3種類以上の色も指定可能で、その場合は3行、4行と指定された色の数ごとに明細行の色が分かれます。

blockでは、上記のコード例のようにグリッド全体や各列のスタイルなどを設定できます。

table()の出力位置は、[0, pdf.cursor]となります。
出力位置を調整する場合は、move_down()bounding_box()を使う必要があります。

データ件数が多く、明細行が改ページ位置にかかった場合は、自動的に改ページされます。

ページトップから出力した場合と、bounding_boxを使って特定の位置に出力する場合の例です。
08_table



















以上、一般的な帳票を作る上で基本となりそうな部分を紹介させていただきました。
これらを組み合わせるだけでも、以下のような簡単な請求書などが作れます。
99_sample



































上図作成時に実際に書いたコードです。
フォントはIPAフォントを使っています。
Prawn::Document.generate("99_sample.pdf",
                        :page_size => 'A4',
                        :page_layout => :portrait,
                        :top_margin => 170,
                        :bottom_margin => 20,
                        :left_margin => 20,
                        :right_margin => 20,
                        :compress => true
                        ) do |pdf|
  total = 0
  data = []
  data.push ["商品名", "単価", "数量", "金額"]
  w = (pdf.bounds.right / 10).floor
  ws = [w*5, w*2, w, w*2]
  for i in 1..20 do
    data.push [
      "商品 #{"%06d" % i}",
      (i * 100).to_s.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse,
      i,
      (i * i * 100).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
      ]
    total += (i * i * 100)
  end

  page_head = 150
  pdf.font "ipam.ttf"
  pdf.repeat :all do
    pdf.font_size = 20
    text = "請   求   書"
    y = pdf.cursor + page_head
    pdf.text_box text, :at => [0, y], :width => pdf.bounds.right, :align => :center
    y -= 40

    pdf.font_size = 16
    pdf.text_box("発行日:#{Time.now.strftime("%Y年%m月%d日")}",
                :at => [0, y],
                :width => pdf.bounds.right,
                :align => :right)
    y -= 25

    w = pdf.bounds.right / 2
    h = pdf.height_of(text) * 3
    pdf.bounding_box [0, y], :width => w, :height => h do
      pdf.text "【宛先】"
      pdf.text "株式会社○○ 様"
    end
    pdf.bounding_box [w, y], :width => w, :height => h do
      pdf.text "【差出人】"
      pdf.text "株式会社□□"
    end

    y -= h
    pdf.font_size = 18
    text = "合計金額: \\#{total.to_s.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse}"
    pdf.text_box(text,
                :at => [0, y],
                :width => pdf.bounds.right,
                :align => :right)
    y -= pdf.height_of("a")
    x1 = pdf.bounds.right - pdf.font.compute_width_of(text)
    x2 = pdf.bounds.right
    pdf.line([x1, y], [x2, y])
    pdf.stroke

    pdf.move_cursor_to pdf.bounds.top
  end

  pdf.table(data,
          :header => true,
          :column_widths => ws,
          :row_colors => ["FFFFFF", "CCCCCC"]
    ) do |t|
    for i in 1..3 do
      t.columns(i).style :align => :right
    end
    t.rows(0).style :align => :center, :background_color => "CCCCCC"
  end
end

ところどころソースを読みながらこのブログを書きましたが、
他にも使えそうなオプションやメソッドがまだまだありそうです。

ソースはgithubに公開されているので、(https://github.com/prawnpdf/prawn)
もっと複雑な帳票を作りたい、という方は是非ソースコードも読んでみてはいかがでしょうか。