Thinreports 0.9.0 で .tlf ファイルの差分が取りやすくなった

2016.6.1 Thinreports 0.9.0 をリリースしました。

Thinreports 0.9.0 is out! | Thinreports - オープンソース PDF 帳票ツール for Ruby, Rails

0.9.0 以降の Editor では、.tlf ファイルが新しい形式で保存されるようになります。これによって、いわゆる「Diff 辛い問題」が解消されます。どういうことか簡単に紹介します。

古い形式

例えば、 test.tlf を Editor で編集して、一つのテキストブロックの文字色を #000000 から #ff0000 へ変更し上書き保存したとします。 0.8.0 以前の古い保存形式の場合、普通に diff を取ると次のような結果となります。

--- a/test.tlf
+++ b/test.tlf
@@ -1 +1 @@
-{"version":"0.8.2","config":{"title":"Test","option":{},"page":{"paper-type":"A4","orientation":"portrait","margin-top":"20","margin-bottom":"20","margin-left":"20","margin-right":"20"}},"svg":"<svg width=\"595.2\" height=\"841.8\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" preserveAspectRatio=\"none\" viewBox=\"0 0 595.2 841.8\"><g class=\"canvas\"><g class=\"s-text\" stroke-width=\"0\" fill=\"#000000\" fill-opacity=\"1\" kerning=\"auto\" letter-spacing=\"normal\" x-display=\"true\" x-id=\"\" id=\"goog_45091349\" font-size=\"18\" font-family=\"IPAMincho\" font-weight=\"normal\" font-style=\"normal\" text-anchor=\"start\" text-decoration=\"none\" x-width=\"72\" x-height=\"20.5\" x-left=\"20\" x-top=\"20\"><rect class=\"s-text-box\" stroke=\"none\" fill=\"#000000\" fill-opacity=\"0.001\" width=\"72\" height=\"20.5\" x=\"20\" y=\"20\"/><text class=\"s-text-l0\" xml:space=\"preserve\" stroke=\"none\" fill=\"inherit\" fill-opacity=\"1\" text-decoration=\"none\" x=\"20\" y=\"36\">\u30bf\u30a4\u30c8\u30eb</text></g><!--SHAPE{\"type\":\"s-tblock\",\"id\":\"text\",\"display\":\"true\",\"desc\":null,\"multiple\":\"false\",\"valign\":\"\",\"line-height\":\"\",\"line-height-ratio\":\"\",\"box\":{\"x\":20,\"y\":56,\"width\":164.1,\"height\":20.5},\"format\":{\"base\":\"\",\"type\":\"\"},\"value\":\"\",\"ref-id\":\"\",\"overflow\":\"\",\"word-wrap\":\"break-word\",\"svg\":{\"tag\":\"text\",\"attrs\":{\"x\":20,\"y\":72,\"xml:space\":\"preserve\",\"kerning\":\"auto\",\"letter-spacing\":\"normal\",\"id\":\"goog_45091350\",\"fill\":\"#000000\",\"fill-opacity\":\"1\",\"font-size\":\"18\",\"font-family\":\"IPAMincho\",\"font-weight\":\"normal\",\"font-style\":\"normal\",\"text-anchor\":\"start\",\"text-decoration\":\"none\"}}}SHAPE--><!--LAYOUT<g xmlns=\"http://www.w3.org/2000/svg\" class=\"s-tblock\" x-format-type=\"\" x-value=\"\" x-format-base=\"\" x-ref-id=\"\" kerning=\"auto\" letter-spacing=\"normal\" x-display=\"true\" x-multiple=\"false\" id=\"goog_45091350\" x-id=\"text\" fill=\"#000000\" fill-opacity=\"1\" font-size=\"18\" font-family=\"IPAMincho\" font-weight=\"normal\" font-style=\"normal\" text-anchor=\"start\" text-decoration=\"none\" x-width=\"164.1\" x-height=\"20.5\" x-left=\"20\" x-top=\"56\"><rect class=\"s-tblock-box\" stroke=\"none\" fill=\"#0096fd\" fill-opacity=\"0.2\" width=\"164.1\" height=\"20.5\" x=\"20\" y=\"56\"/><text class=\"s-tblock-id\" font-size=\"10.5\" font-family=\"Helvetica\" font-weight=\"normal\" font-style=\"normal\" text-decoration=\"none\" text-anchor=\"start\" kerning=\"auto\" stroke=\"none\" fill=\"#0096fd\" fill-opacity=\"1\" x=\"24\" y=\"67\">text</text></g>LAYOUT--></g></svg>","state":{"layout-guide":[]}}
\ No newline at end of file
+{"version":"0.8.2","config":{"title":"Test","option":{},"page":{"paper-type":"A4","orientation":"portrait","margin-top":"20","margin-bottom":"20","margin-left":"20","margin-right":"20"}},"svg":"<svg width=\"595.2\" height=\"841.8\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" preserveAspectRatio=\"none\" viewBox=\"0 0 595.2 841.8\"><g class=\"canvas\"><g class=\"s-text\" stroke-width=\"0\" fill=\"#000000\" fill-opacity=\"1\" kerning=\"auto\" letter-spacing=\"normal\" x-display=\"true\" x-id=\"\" id=\"goog_45091349\" font-size=\"18\" font-family=\"IPAMincho\" font-weight=\"normal\" font-style=\"normal\" text-anchor=\"start\" text-decoration=\"none\" x-width=\"72\" x-height=\"20.5\" x-left=\"20\" x-top=\"20\"><rect class=\"s-text-box\" stroke=\"none\" fill=\"#000000\" fill-opacity=\"0.001\" width=\"72\" height=\"20.5\" x=\"20\" y=\"20\"/><text class=\"s-text-l0\" xml:space=\"preserve\" stroke=\"none\" fill=\"inherit\" fill-opacity=\"1\" text-decoration=\"none\" x=\"20\" y=\"36\">\u30bf\u30a4\u30c8\u30eb</text></g><!--SHAPE{\"type\":\"s-tblock\",\"id\":\"text\",\"display\":\"true\",\"desc\":null,\"multiple\":\"false\",\"valign\":\"\",\"line-height\":\"\",\"line-height-ratio\":\"\",\"box\":{\"x\":20,\"y\":56,\"width\":164.1,\"height\":20.5},\"format\":{\"base\":\"\",\"type\":\"\"},\"value\":\"\",\"ref-id\":\"\",\"overflow\":\"\",\"word-wrap\":\"break-word\",\"svg\":{\"tag\":\"text\",\"attrs\":{\"x\":20,\"y\":72,\"xml:space\":\"preserve\",\"kerning\":\"auto\",\"letter-spacing\":\"normal\",\"id\":\"goog_45091350\",\"fill\":\"#ff0000\",\"fill-opacity\":\"1\",\"font-size\":\"18\",\"font-family\":\"IPAMincho\",\"font-weight\":\"normal\",\"font-style\":\"normal\",\"text-anchor\":\"start\",\"text-decoration\":\"none\"}}}SHAPE--><!--LAYOUT<g xmlns=\"http://www.w3.org/2000/svg\" class=\"s-tblock\" x-format-type=\"\" x-value=\"\" x-format-base=\"\" x-ref-id=\"\" kerning=\"auto\" letter-spacing=\"normal\" x-display=\"true\" x-multiple=\"false\" id=\"goog_45091350\" x-id=\"text\" fill=\"#ff0000\" fill-opacity=\"1\" font-size=\"18\" font-family=\"IPAMincho\" font-weight=\"normal\" font-style=\"normal\" text-anchor=\"start\" text-decoration=\"none\" x-width=\"164.1\" x-height=\"20.5\" x-left=\"20\" x-top=\"56\"><rect class=\"s-tblock-box\" stroke=\"none\" fill=\"#0096fd\" fill-opacity=\"0.2\" width=\"164.1\" height=\"20.5\" x=\"20\" y=\"56\"/><text class=\"s-tblock-id\" font-size=\"10.5\" font-family=\"Helvetica\" font-weight=\"normal\" font-style=\"normal\" text-decoration=\"none\" text-anchor=\"start\" kerning=\"auto\" stroke=\"none\" fill=\"#0096fd\" fill-opacity=\"1\" x=\"24\" y=\"67\">text</text></g>LAYOUT--></g></svg>","state":{"layout-guide":[]}}
\ No newline at end of file

これは辛い。主な原因は以下の通りです。

  1. 改行が取り除かれ、一行になっている
  2. レイアウトデザインは SVG がそのまま保存されている

(2) について少し説明が必要だと思います。.tlf の中身は JSON ではありますが、図形の位置やスタイルなどのデザインの状態は、SVG 形式で文字列として保存されています。下記は古い形式の .tlf の中身をわかりやすく整形したものですが、"svg" キーの値がそれです。

{
  "version": "0.8.2",
  "config": {
    "title": "Report Title",
    "page": {
      "paper-type": "A4",
      "orientation": "landscape",
      "margin-top": 0.0,
      "margin-bottom": 0.0,
      "margin-left": 0.0,
      "margin-right": 0.0,
    }
  },
  "svg": "<svg><g class=\"canvas\"><!--LAYOUT<rect x-id=\"rect_id\" x=\"100.0\" y=\"100.0\" width=\"200\" height=\"200\"/>--><!--SHAPE{ "type": "s-text", "id": "rect_id", "display": "true", "svg": { "attrs": {...} }}SHAPE--></g></svg>"
} 

当然、文字色の情報も "svg" キーの中のどこかに記録されていますが、単純な diff でその変更箇所を探すことは現実的ではありませんでした。

新しい形式

一方、新しい形式で diff を取ると以下のようになります。

--- a/test.tlf
+++ b/test.tlf
@@ -41,7 +41,7 @@
           "IPAMincho"
         ],
         "font-size": 18,
-        "color": "#000000",
+        "color": "#ff0000",
         "text-align": "left",
         "vertical-align": "top",
         "line-height": "",

一目瞭然。

新しい形式の詳細は下記で詳しく説明しています。興味がある方はどうぞ。

Diff が取れて何が嬉しいのか

いろいろあると思いますが、

  • git などの VCS で tlf ファイルの変更履歴が管理しやすくなる
  • Pull Request 等、コードレビューが捗る

などでしょうか。コードレビューが捗る、というか可能になったことは個人的にも本当に助かります。

まとめ

新しい形式に変更した理由は他にもありますが、その一つとして「Diff 辛い問題の解消」について説明しました。 今回の変更によって、Thinreports を使った開発が少しでもやりやすくなれば幸いです。

Thinreports for PHP を使って PHP7 で帳票や PDF を作る

これは PHP Advent Calendar 2015 21日目の記事です。

Thinreports for PHP については、まだ alpha 版だったときに下記 Qiita エントリーでも紹介していましたが、先日 0.8.0 正式版をリリースしたので PHP7 でのサンプルコードや、alpha 版からの変更点などを含め、改めて紹介したいと思います。

qiita.com

Thinreports for PHP とは

まず、Thinreports (シンレポーツ)について説明する必要があります。

Thinreports

Thinreports は、2010年ごろオープンソースとしてリリースされた、Ruby 向けの帳票・PDFツールです。下記のような特徴があります。

  • 国産 OSS
  • レイアウトデザインツール Thinreports Editor と、レイアウトファイルを読み込んで PDF を生成する Ruby 向けライブラリ Thinreports Generator for Ruby がセット

詳しくは 公式サイト や下記 GitHub をご覧ください。

github.com

Thinreports for PHP

今回紹介する Thinreports for PHP は、この Thinreports における PDF 生成ライブラリの PHP 実装ということになります。もちろん、オープンソースとして開発が進められています。

github.com

下記のような特徴があります。

  • バージョン 0.8.0 (2015.12.21 現在)
  • PHP 5.3 以降と 7 に対応
  • 日本語標準対応(IPAフォントをビルトイン)

PDF 生成までの流れ

Thinreports は mPDF のような HTML to PDF ではなく、別のアプローチを採用しています。大まかには下記のような流れで PDF を生成します。

  1. デザインツール Thinreports Editor を使って帳票レイアウトファイルを作成
  2. レイアウト作成時に、値を埋め込みたいところに名前付きの領域(テキストボックスみたいなもの)を定義しておく
  3. 作成したレイアウトファイルを Thinreports for PHP で読み込んで、名前付きの領域に値を埋め込むなどして PDF を生成するコードを書く

Hello World

では実際にやってみます。今回はせっかくなので PHP7.0.1 で試してみます。Thinreports for PHPREADME.md でも簡単に説明していますので、そちらも参考にしてください。

準備

PHP7.0.1 と Composer がインストールされていることが前提です。これらのインストール手順については割愛します。PHP については、サポートしている 5.3+ であれば問題ありません。

Step1. Thinreports Editor をインストール

Editor は Chrome アプリとして動作します。Google Chrome をインストールしていない場合はインストールしてください。Chrome の準備ができたら Chrome ウェブストア (無料)にアクセスして、右上の "CHROME に追加" をクリックすると Thinreports Editor がインストールできます。 このとき、 Google アカウントは必ずしも必要ではありません。

Step2. 帳票レイアウトファイルを作成

まず、Thinreports Editor を起動して、新規作成ボタンから新しいレポートを作成します。ここでは、用紙サイズや余白などはデフォルトのままとして OK をクリックします。

f:id:hidakatsuya:20151221000936p:plain

続いて、下記のようにテキストブロックを貼り付けて、フォントサイズを 48 にして、ID を hello_world としておきます。

f:id:hidakatsuya:20151221001123p:plain

保存ボタンを押すと、保存先を指定するダイアログが表示されるので、作成したレイアウトファイルを任意の場所に保存します。ここではデスクトップに hello_world ディレクトリを作成して、その中に layout.tlf という名前で保存したとします。つまり ~/Desktop/hello_world/layout.tlf に保存したことになります。

Step3. thinreports-php/thinreports-php のインストール

$ cd ~/Desktop/hello_world
$ composer require thinreports-php/thinreports-php

Step4. PDF 生成コードを書く

以下の内容で ~/Desktop/hello_world/hello_world.php を作成します。

<?php
require 'vendor/autoload.php';

$report = new Thinreports\Report('layout.tlf');

# 1st page
$page = $report->addPage();
$page->item('hello_world')->setValue('Hello World');

# 2nd page
$page = $report->addPage();
$page('hello_world')->setValue('Hello Thinreports')
                    ->setStyle('color', '#ff0000');

$report->generate('hello_world.pdf');

Step5. 実行する

$ cd ~/Desktop/hello_world
$ php hello_world.php

成功すると ~/Desktop/hello_world/hello_world.pdf に以下のような PDF ファイルが作成されます。

f:id:hidakatsuya:20151221011227p:plain

Quick Reference

この他にも色々なことができます。まだ公式リファレンスをまとめていないので、ここで逆引き形式の簡単なリファレンスをまとめておくことにします。

けい線や図形、テキストのスタイルを動的に操作する

Editor で描画した図形やテキストのスタイルを PHP で動的に変更することができます。

<?php
// なんでも
$page->item('any_object')->hide();
$page->item('any_object')->show();
$page->item('any_object')->setVisible(true);

// 四角形, 楕円形, 線形
$page->item('rect_id')->setStyle('border_width', 1)
                      ->setStyle('border_color', '#0000ff')
                      ->setStyle('fill_color', '#ff0000')

// テキスト, テキストブロック
$page->item('text_id')->setStyles(array(
    'color'       => 'blue',
    'align'       => 'center', // left, center, right
    'valign'      => 'bottom'  // top, center, bottom
    'font_size'   => 20,
    'bold'        => true,
    'italic'      => false,
    'linethrough' => true,
    'underline'   => false
));

複数のレイアウトを組み合わせる

Editor で複数のレイアウトを作成し、それらを使って一つの PDF ファイルを作成することも可能です。例えば、表紙のレイアウト cover.tlf と内容のレイアウト body.tlf で表紙付きの PDF を作成する場合は以下のように書きます。

<?php
# 内容のレイアウト body.tlf をデフォルトレイアウトとして指定
$report = new Thinreports\Report('body.tlf');

# 表紙のレイアウト cover.tlf でページを作成
$cover = $report->addPage('cover.tlf');
$cover('title')->setValue('Title');

# 内容のレイアウト body.tlf でページを作成
# `#addPage` の第一引数を省略した場合は、デフォルトレイアウトでページを作成する
$body = $report->addPage();
$body('content')->setValue('何かの内容');

$report->generate('result.pdf');

PDF データを取得する

PDF ファイルを作るのではなく、PDF データを取得することができます。

<?php
# :
$report->generate(); # => PDF data

日本語を出力する

Thinreports は、標準で日本語出力に対応しているため、レイアウト作成時に日本語フォントを設定すれば正しく出力されます。

f:id:hidakatsuya:20151221015157p:plain

ただし、選択できる日本語フォントは IPAフォントのみであることに注意してください。現状、任意のフォントを指定することはできません。

以上です。その他にも

  • ページ番号ツールのフォーマットを変更する
  • ページ毎に、ページ数のカウントから除外する
  • 空白のページを追加する
  • 簡易書式の設定を動的に変更する
  • 一覧表っぽいものを作る

など、まだまだいろいろな機能がありますが、これらについてはまた別の機会で紹介したいと思います。 本当は laravel で簡単な帳票アプリを作る方法を紹介したかったのですが。これについてもまた別の機会に。

最後に

いかがでしたでしょうか?PHP7 で実際に動かしたのは今回初めてだったのですが、ちゃんと動くようで安心しました(テストコードはもちろんパスしてる)。ぜひ、PHP7 と共に触っていただけると嬉しいです。不具合などは GitHub へどうぞ。

明日は nori0620 さんです。

参考リンク

株式会社 Misoca に入社しました

こんにちは、@hidakatsuya です。

2015年10月1日、 株式会社 Misoca に入社しました。

www.misoca.jp

Misoca では、島根県出雲大社があって砂丘が無い方)からリモートで 請求書作成サービス「Misoca(ミソカ)」 の開発に携わっています。詳しいことについては Misoca開発ブログ など、また別の機会にまとめるとして、今回は報告までにしておきたいと思います。

今後とも Misoca 共々よろしくお願いいたします。

誰?

最後に簡単に自己紹介しておきます。

github.com

fixtures のグループを作ることができる fixture_group という gem を作った

github.com

Rails には fixtures というテストデータを管理する機能が標準であります。今回作った fixture_group はその fixtures の拡張で、名前の通り、fixtures のグループを作ってグループ単位で fixtures をロードしたりできるようになるライブラリです。

使い方

README.md の説明そのままですが、例えば次のような感じで使うことができます。

fixtures グループの構成:

Rails.root/
  |- app/
  |- config/
  |    :
  `- test/
    |- unit/
    |    :
    `- fixtures/
      |- group_a/
      |  |- users.yml
      |  `- items.yml
      `- group_b/
         |- subgroup_b1/
         |  |- users.yml
         |  `- items.yml
         `- subgroup_b2/
            |- users.yml
            `- items.yml

テストコードで fixtures をロードする:

class FooTest < ActiveSupport::TestCase
  fixture_group 'group_a', :all

  def test_index
    # :
    assert_equal users(:user_a), @user
  end
end

class HogeIntegrationTest < ActionDispatch::IntegrationTest
  fixture_group 'group_b/subgroup_b1', :users, :items
  # :
end

README でもこの程度しか説明していないので、詳しい使い方は下記テストコードを見てください。ちなみに、関連とか ERB など、通常の fixtures の機能も普通に使えると思います。

fixture_group/test at master · hidakatsuya/fixture_group · GitHub

インストール

Gemfile に下記の行を追記:

gem 'fixture_group', group: :test, require: false

インストール:

$ bundle

test_helper.rb などで読み込む:

require 'fixture_group'

既知の問題点

通常、fixtures は test/fixtures のようなディレクトリ内にテーブルごとの yml ファイルを作成すると思います。その場合、実テーブルとそれに対応する yml ファイルは 1:1 なので、fixtures 標準では一度ロードしたテーブルデータはキャッシュされる仕組みになっています。

しかし、fixture_group を使う場合、実テーブルと yml ファイルが 1:1 にならないため、強制的に標準のキャッシュ機能を off にする必要がありました。厳密には、キャッシュさせないようにしてるのではなく、 このコード でやってるように、キャッシュされてるかどうかの問い合わせに対して常に No と返事する*1ようにモンキーパッチしています

ベンチマークしてないのでどの程度かわかりませんが、fixture_group を使わない場合に比べるとほぼ間違いなく遅くなると思います。良い方法は全く思いつかないですが、解決できるといいなぁと思ってはいます。

*1:fixture_group 管理外の yml は除く