昨日、 RD (るびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。
Rabbitではスライドを画像 やHTML(+画像) 、 PDF (一覧表示 )などで出力することができます。
発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。
RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。
スライドをテキストで作成すると以下のような利点があります。
一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointや wikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。
RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。
そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。
Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。
Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。
現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。
ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalize と Ruby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。
Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。
Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。
Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。
ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。
ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。
% rails shelf % cd shelf % script/generate resource book title:string % rake db:migrate % script/plugin install git://github.com/activescaffold/active_scaffold.git % script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git
config/routes.rb:
1 2 |
- map.resources :books + map.resources :books, :active_scaffold => true |
app/controllers/books_controller.rb:
1 2 3 |
class BooksController < ApplicationController active_scaffold :book end |
app/views/layouts/application.html.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title>ActiveScaffold l10n</title> <%= javascript_include_tag(:defaults) %> <%= active_scaffold_includes %> </head> <body> <h1>ActiveScaffold l10n</h1> <%= yield %> </body> </html> |
app/controllers/applications.rb:
1 2 3 4 5 6 7 8 9 |
class ApplicationController < ActionController::Base # ... private before_filter :localize_active_scaffold def localize_active_scaffold ActiveScaffold::Localization.lang = "ja-jp" true end end |
サーバを起動してhttp://localhost:3000/books/にアクセスします。
% script/server % firefox http://localhost:3000/books/
見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。
ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb*1を作成します。
config/initializers/lang/ja-jp.rb:
1 2 3 4 5 6 7 |
# -*- coding: utf-8 -*- ActiveScaffold::Localization.define('ja-jp') do |lang| lang["Books"] = "本一覧" lang["Book"] = "本" lang["Title"] = "タイトル" end |
config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。
(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)
ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。
ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。
ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。
また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。
config/environment.rb:
1 2 3 4 5 6 |
# ... Rails::Initializer.run do |config| # ... config.gem "gettext", :lib => "gettext/rails" # ... end |
lib/active_scaffold_gettext.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
module ActiveScaffoldGetText include GetText::Rails bindtextdomain(GETTEXT_DOMAIN) end class Object def as__with_gettext(message, *args) return nil if message.nil? localized_message = ActiveScaffoldGetText.send(:sgettext, message) if localized_message == message as__without_gettext(message, *args) else localized_message % args end end alias_method_chain :as_, :gettext end module ActiveScaffold::DataStructures class Column def initialize_with_gettext(name, active_record_class) initialize_without_gettext(name, active_record_class) self.label = "#{active_record_class.name.demodulize}|#{@label.humanize}" end alias_method_chain :initialize, :gettext end end |
config/initializers/gettext.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
GETTEXT_DOMAIN = "your-rails-application" require 'active_scaffold_gettext' class ActiveRecord::Errors # restore default error messages overridden by Ruby-GetText-Package. @@default_error_messages = { :inclusion => "is not included in the list", :exclusion => "is reserved", :invalid => "is invalid", :confirmation => "doesn't match confirmation", :accepted => "must be accepted", :empty => "can't be empty", :blank => "can't be blank", :too_long => "is too long (maximum is %d characters)", :too_short => "is too short (minimum is %d characters)", :wrong_length => "is the wrong length (should be %d characters)", :taken => "has already been taken", :not_a_number => "is not a number", :greater_than => "must be greater than %d", :greater_than_or_equal_to => "must be greater than or equal to %d", :equal_to => "must be equal to %d", :less_than => "must be less than %d", :less_than_or_equal_to => "must be less than or equal to %d", :odd => "must be odd", :even => "must be even" } alias_method :on, :on_without_gettext alias_method :[], :on end |
lib/tasks/gettext.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
namespace :gettext do namespace :po do desc "Update pot/po files." task :update => :environment do require 'gettext/utils' module GetText::ActiveRecordParser class << self alias_method :add_target_original, :add_target def add_target(targets, file, msgid) if /\|/ !~ msgid add_target_original(targets, file, msgid.classify) add_target_original(targets, file, msgid.classify.pluralize) end add_target_original(targets, file, msgid) end end end targets = Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}") GetText.update_pofiles(GETTEXT_DOMAIN, targets, "#{GETTEXT_DOMAIN} 0.0.1") end end namespace :mo do desc "Create mo-files" task :create do require 'gettext/utils' GetText.create_mofiles(true, "po", "locale") end end end |
app/controllers/application.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ApplicationController < ActionController::Base init_gettext GETTEXT_DOMAIN # ... private before_filter :localize_active_scaffold def localize_active_scaffold posix_locale = GetText.locale.to_posix posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale lang = posix_locale.gsub(/_/, '-').downcase ActiveScaffold::Localization.lang = lang true end end |
翻訳メッセージのファイルpoを作って翻訳します。
% rake gettext:po:update % mkdir po/ja % msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP # 途中でメールアドレスを聞かれるので入力する
po/ja/your-rails-application.po:
# ... #: app/models/book.rb:- msgid "Book" msgstr "本" #: app/models/book.rb:- msgid "Books" msgstr "本一覧" # ... #: app/models/book.rb:- msgid "Book|Title" msgstr "タイトル" # ...
翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。
% rake gettext:mo:create
config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。
ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。
基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。
*1 config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。
ちょうど1ヶ月前の話の続きです。
前回でCutterでテストを作成するための環境ができたので、実際にテストを作成していきます。と、思ったのですが、もう一点やらなければいけないことが残っていました。テスト対象のライブラリの初期化についてです。
今回はテスト対象ライブラリの初期化について説明してからテスト作成に入ります。
前回同様、コードの断片がでてきます。完全なものはSennaのリポジトリを見てください。
Sennaのようにライブラリ初期化・終了関数 (sen_init()/sen_fin())を用意している場合は、テストの作成に 入る前に、もう一つ用意しておかなければいけない仕組みがありま す。このような関数を持っているライブラリをテストする場合は、 テスト全体を実行する前に初期化関数を、実行した後に終了関数を 呼び出す必要があります。これを行う仕組みを用意する必要があり ます。
cutterコマンドは指定されたディレクトリ以下の共有ライブラリを かき集めて、その中からテストを検出して実行します。その時に以 下の条件にあう共有ライブラリを見つけると、テスト全体を実行す る前後に特定の関数を実行することができます。これは、今まさに 必要としている機能です。
この共有ライブラリが以下の名前のシンボルを公開している場合は、 その関数をテスト全体を実行する前後に実行します。ここでは、共 有ライブラリのファイル名はsuite_senna_test.soとします。
「_warmup」と「_cooldown」の前の「senna_test」の部分は共有ラ イブラリのファイル名から先頭の「suite_」と拡張子を除いた部分 です。
Sennaの場合は以下のようなsuite-senna-test.cを作成します。
test/unit/suite-senna-test.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <senna.h> void senna_test_warmup(void); void senna_test_cooldown(void); void senna_test_warmup(void) { sen_init(); } void senna_test_cooldown(void) { sen_fin(); } |
suite-senna-test.cをビルドするためにMakefile.amに以下を追加 します。
test/unit/Makefile.am:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
if WITH_CUTTER ... noinst_LTLIBRARIES = \ suite_senna_test.la endif INCLUDES = \ -I$(srcdir) \ -I$(top_srcdir) \ -I$(top_srcdir)/lib \ $(SENNA_INCLUDEDIR) AM_CFLAGS = $(GCUTTER_CFLAGS) AM_LDFLAGS = -module -rpath $(libdir) -avoid-version LIBS = \ $(top_builddir)/lib/libsenna.la \ $(GCUTTER_LIBS) suite_senna_test_la_SOURCES = suite-senna-test.c |
よくあるMakefile.amの書き方です。noinst_LTLIBRARIESをif WITH_CUTTER ... endifの中に入れているのは、Cutterがない環境で はビルド対象からはずし、ビルドエラーにならないようにするため です。
これで、test/unit/.libs/suite_senna_test.soがビルドされるよう になり、テスト全体を実行する前後にSennaの初期化・終了処理を行 うことができます。
テスト実行環境が整ったのでテストを作成します。ここでは検索キー ワードの周辺テキストを取得するsnippet APIのテスト を1つ作成します。
テストの流れは以下の通りです。
基本的なCutterのテスト作成方法についてはチュートリアル を参考にしてください。
まずは、sen_snip_open()でsen_snipオブジェクトを生成する部分 と、sen_snip_close()で生成したsen_snipオブジェクトを開放する 部分を作成します。
今回はGLibサポート付きでCutterを使用するgcutterパッケージを 使っているので、テストは以下のようにgcutter.hを利用します。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <senna.h> #include <gcutter.h> void test_simple_exec(void); static const gchar default_open_tag[] = "[["; static const gchar default_close_tag[] = "]]"; void test_simple_exec(void) { sen_snip *snip; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); sen_snip_close(snip); } |
sen_snip_open()は引数が多いですが、ここでは気にする必要はあ りません。sen_snip_open()によりsen_snip *が生成されることだけ知っ ていれば問題ありません。
cut_assert_not_null(snip) でsen_snip *が正常に生成されているかを確認します。これは、 sen_snip_open()は失敗時にはNULLを返すからです。
最後にsen_snip_close()で生成したsen_snip *を開放します。
次はsen_snip_add_cond()でキーワードを指定する処理を追加しま す。sen_snip_add_cond()の戻り値はsen_rcです。sen_rcはエラー 番号を示す数値でsen_success(0)以外はエラーになります。よって テストは以下のようになります。sen_snip_open()のときと同じく、 sen_snip_add_cond()の引数は気にしなくても構いません。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... void test_simple_exec(void) { sen_snip *snip; const gchar keyword[] = "Senna"; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); sen_snip_close(snip); } |
sen_snip_add_cond()の結果は cut_assert_equal_int で検証しています。
ただし、ここで問題があります。cut_assert*()は検証が失敗する とその時点でテスト関数からreturnし、それ以降のコードは実行し ません。つまり、cut_assert_equal_int()が失敗した場合は、 sen_snip_open()で生成したsen_snip *が開放されないことになり ます。この問題を解決するためにsetup()/teardown()という仕組み があります。
setup()はテストが実行される前に必ず実行され、teardown()はテ ストが実行された後に成功・失敗に関わらず必ず実行されます。こ の仕組みを利用することで確実にメモリ開放処理を行うことができ ます。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
... static sen_snip *snip; ... void setup(void) { snip = NULL; } void teardown(void) { if (snip) { sen_snip_close(snip); } } void test_simple_exec(void) { const gchar keyword[] = "Senna"; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); } |
これでcut_assert_equal_int()が成功しても失敗してもsen_snip * は開放されます。Cutterではメモリ開放処理のためにstatic変数と setup()/teardown()を使うことが定石になっています。
次はsen_snip_add_cond()で設定したキーワード用のsnippetを生成 するsen_snip_exec()のテストです。sen_snip_exec()もsen_rcを返 すので、それを検証します。また、引数でsnippet数とsnippet文字 列のバイト数も受けとるのでそれも検証します。特に目立った部分 はありません。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
... static const gchar text[] = "Senna is an embeddable fulltext search engine, which you can use in\n" "conjunction with various scripting languages and databases. Senna is\n" "an inverted index based engine, & combines the best of n-gram\n" "indexing and word indexing to achieve fast, precise searches. While\n" "senna codebase is rather compact it is scalable enough to handle large\n" "amounts of data and queries."; ... void test_simple_exec(void) { ... unsigned int n_results; unsigned int max_tagged_len; ... cut_assert_equal_int(sen_success, sen_snip_exec(snip, text, strlen(text), &n_results, &max_tagged_len)); cut_assert_equal_uint(2, n_results); cut_assert_equal_uint(105, max_tagged_len); } |
最後はsen_snip_exec()で生成したsnippetの内容が正しいかどうか のテストです。snippetはsen_snip_get_result()で取得できるので その結果を検証します。n_resultsが2なので2回 sen_snip_get_result()を呼び出す必要があります。
snippetを格納する場所のサイズは動的に決まります。そのため、 snippetを格納する領域を動的に確保する必要があります。 setup()/teardown()の仕組みを用いてメモリを開放するようにしま す。ここ以外は特に目立った部分はありません。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
... static gchar *result; void setup(void) { ... result = NULL; } void teardown(void) { ... if (result) { g_free(result); } } void test_simple_exec(void) { ... unsigned int result_len; ... result = g_new(gchar, max_tagged_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 0, result, &result_len)); cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, " "which you can use in\n" "conjunction with various scripti", result); cut_assert_equal_uint(104, result_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 1, result, &result_len)); cut_assert_equal_string("ng languages and databases. [[Senna]] is\n" "an inverted index based engine, & combines " "the best of n-gram\ni", result); cut_assert_equal_uint(104, result_len); } |
これで単純にsnippet APIを使った場合のテストが1つできました。 同様に、異常な場合や違ったデータを用いた場合などのテストを作 成していきます。
せっかくなのでCutterのテスト結果の出力方法を紹介します。
Cutterは cut_assert_equal_string で文字列の比較が失敗したときには、どの部分が異なったかという 差分情報を表示します。
例えば、今回のテストの最後のcut_assert_equal_string()が失敗 した場合は以下のような差分情報が表示されます。
diff: - ng languages and DBes. [[Senna]] is ? ^^ + ng languages and databases. [[Senna]] is ? ^^^^^^^ - an Inverted Index Based Engine, & combines the best of n-gram ? ^ ^ ^ ^ + an inverted index based engine, & combines the best of n-gram ? ^ ^ ^ ^ i
このときの期待した結果は以下の通りです。
ng languages and DBes. [[Senna]] is an Inverted Index Based Engine, & combines the best of n-gram i
実際の結果は以下の通りです。
ng languages and databases. [[Senna]] is an inverted index based engine, & combines the best of n-gram i
差分を見てもらうと分かる通り、異なっている行を示すだけではな くて、行内で異なっている文字まで示しています。(例えば、DBの 下に^^が付いている。)
広く使われているunified diff形式では行内で異なる文字は表示し ません。テストでは1行のみの比較を行うことも多く、行単位だけ の差分よりも文字単位での差分表示も行った方がデバッグが行いや すいという判断からこのような形式になっています。
この形式はPythonのdifflibにあるndiffの形式と同じものです。
今回作成したテストは以下の通りです。
test/unit/test-snip.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
#include <senna.h> #include <gcutter.h> void test_simple_exec(void); static sen_snip *snip; static const gchar default_open_tag[] = "[["; static const gchar default_close_tag[] = "]]"; static const gchar text[] = "Senna is an embeddable fulltext search engine, which you can use in\n" "conjunction with various scripting languages and databases. Senna is\n" "an inverted index based engine, & combines the best of n-gram\n" "indexing and word indexing to achieve fast, precise searches. While\n" "senna codebase is rather compact it is scalable enough to handle large\n" "amounts of data and queries."; static gchar *result; void setup(void) { snip = NULL; result = NULL; } void teardown(void) { if (snip) { sen_snip_close(snip); } if (result) { g_free(result); } } void test_simple_exec(void) { const gchar keyword[] = "Senna"; unsigned int n_results; unsigned int max_tagged_len; unsigned int result_len; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); cut_assert_equal_int(sen_success, sen_snip_exec(snip, text, strlen(text), &n_results, &max_tagged_len)); cut_assert_equal_uint(2, n_results); cut_assert_equal_uint(105, max_tagged_len); result = g_new(gchar, max_tagged_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 0, result, &result_len)); cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, " "which you can use in\n" "conjunction with various scripti", result); cut_assert_equal_uint(104, result_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 1, result, &result_len)); cut_assert_equal_string("ng languages and databases. [[Senna]] is\n" "an inverted index based engine, & combines " "the best of n-gram\ni", result); cut_assert_equal_uint(104, result_len); } |
問題発生時に有用なデバッグ情報を増やしたり、より読みやすいテ ストにするなど、いろいろ改良するべき点は残っていますが、今回 はこれで終了します。実際のコードはSennaのリポジトリを参照 してください。
2回に分けて以下のことについて説明しました。
Cで書かれたプロジェクトに単体テストフレームワークを導入する 場合はCutterも検討してみてはいかがでしょうか。