書体見本誌をTeXで連結して作った話 (TeX/LaTeX Advent Calendar 2015)

この記事は、TeX & LaTeX Advent Calendar 2015 11日目の記事です。
昨日はabenoriさん、明日はaminophenさんです。



わたしとTeX

TeX未経験でした。実は現在もTeXが書けません。必要な機能をWebから拾ってきて、スクリプトにしたあとはすべて忘れてしまったからです。
だからこの記事は備忘録ですらなく、TeXコミュニティの皆様に、TeXが使えないTeX無関係者はこのようにTeXを"使う"んですよ、多分。とご報告させて頂ければと思った次第です。



TeXを何に使ったか

RuneAMNというフォント製品で、書体見本誌の生成に使いました。
ざっくりと説明しますと、RuneAMNはイラスト・デザイン向けのルーン文字フォントです。
project daisy bellがリリースしており、フリー版と製品版があります。


 

TeXを使ったのは、RuneAMNフォントの書体見本誌の作成作業です。
RuneAMN_Freeリリース後、諸事情により作ったRuneAMN_Proを販売開始するにあたって、書体見本誌が存在することは重要でした。

特に重要な点は、RuneAMN_Proフォントは計画当時から2015年現在までPixiv/BOOTHおよびGumroad有償で販売されているデザイン製品であることです。
公開・添付される書体見本誌も、本物のデザイナ・イラストレータが1600円を払う価値を認めてくれる、本物の書体見本誌でなければいけません。
トレッキーなギークがプログラマ向けに作るAPIドキュメントのようなものでは駄目なのです。
無地か灰色を背景に使い、コードが読みやすいという理由で等幅なConsolas書体を選び、小さな黒文字で書かれたドキュメントをKISSにつき良し、とするわけにはいきませんでした。(私はそちらのほうが好みなので、少しそういう要素を残してありますが)


計画は単純です。
・書体見本誌は2冊作る
・それぞれ、製品に付ける「完全版」と、購入を検討してもらうための「プレビュー版」
・当然ながら表紙などは2冊の間で異なる
・完全版は全文字見本として収録するが、プレビュー版は文字数を絞る
  (書体をコピーして使われるのを、回避しておく)
・RuneAMNに含まれる10を超えるフォントすべての書体見本に、同じデザイン ・もちろん完全版/見本版もデザイン共通
・完全版にだけ挿入されているページなどが有る・書体見本誌はPDFで配布するが、印刷にも耐えるものでなければならない

デザイン統一は見た目もありますが、労力を省くためでもあります。

TeXをどう使ったか

タイトルに反して、TeXをどう使わなかったかという話になりますが、結局、版組にはTeXを使いませんでした。



実は、印刷可能なドキュメントの自動生成ということで、真っ先にTeXを検討しました。
しかし、すぐに諦めることになりました。
・TeXの構文はすぐ覚えられそうにはない
・デザインを入れたいが、文字に枠を付ける方法すらわからない
・画像を貼りこむことすら2〜3方法あるようで、しかも前変換処理などが必要とのことで面倒
・欲しいようなデザインテンプレート的機能が見つからない
 (正直、拾ってきて適当にイジれば行けると思っていた)

以上、生TeXが使えない状態では、ScribasなどのWYSIWYGエディタにも頼れませんでした。
TeXが読めない私には、TeXエディタに背中を預ける勇気が持てませんでした。
(特にプログラマとして"抽象化には漏れが有る"ことを知っている身としては余計に。正直アレの信頼性ってどの程度か教えて頂けませんか?)
機械生成されたコメントも可読性も無い"生TeX"を読まされる未来がありありと想像できました。



初心者がいきなりTeXで"デザイン"は敷居が高かったようです。
『TeX初心者がいきなり製品デザインに突貫するなよ』という話でもありますが。

結局、使い慣れたIllustratorで1ページ1ファイルとして作りました。
AIで作ったPDFを、Webサービスで連結して見本誌を製本していました。
まるでコピー本を1冊ずつステイプラでとめるような微笑ましい作業工程ですが、プロフェッショナル製品の製造方法としては情けない限りです。
ともあれ、最初のリリースはそれで済ませました。
しかし、修正がある度に、23枚のPDFをアップロードし直すのはとても手間。
次のリリースではPDFの合成をスクリプトで一発にしたかったので、TeXスクリプトの作成が最初から計画に入っていました。
TeXにはPDFを操作する機能が一応ある。
AIで作ったPDFはTeX生データほど構造化されていないわけですが。
PDFファイルをページとして扱って、繋げるくらいのことはできます。

作ったTeXスクリプト

スクリプトと生成物が公開状態で置いてあると、説明が"読めば分かります"で済むので助かります。
ドキュメントよりサンプル派です。

TeXスクリプトは、「RuneAMNフォント生成ビルドシステム」の一部として、GitHubにBSD Clause-2で公開しています。
生成物はこちらの「RuneAMN_Pro書体見本誌(PDF)

基本構成は、ベースTeXスクリプトからbash差分を生成して叩く仕組みです。
詳しくは、呼び出し元 bashスクリプト
RuneAMN_Pro_Series_Fonts/scripts/books_build.sh
を起動すると、TeXスクリプト
RuneAMN_Pro_Series_Fonts/scripts/mods/book_of_RuneAMN_Pro_Fonts.tex
を『製品版』『プレビュー版』書き換えて、書き換えたTeXスクリプトを上記bashスクリプトが叩く仕組みになっています。


TeXはページ生成どころか加工すらせず、PDFの連結にしか使われていません。
連結するファイルをスクリプトに直書きしているので、書体見本ページが増えると書き直しです。
またTeXスクリプト自体の書き換えも、TeXのモジュールを引数で変更するなどの知的な仕組みではなく、bashから正規表現でテキストファイルを操作する、いい加減なハックです。

ゴーストスクリプトなどを使う予定はないので、PDFを生成した後は、中間生成物は不要ファイルとして削除しています。
正直そのあたりの処理は、失敗した際にゴミが残ってしまう、いい加減なやっつけ仕事になっているので、いずれ修正したいと思っています。


TeXをほとんど使っていませんね。
とてもTeX & LaTeX Advent Calendar 2015 とは思えません。
もっとエレガントに、同じことをする方法があると思いますが。
ともかくこうして書体見本誌は完成し、RuneAMNは無事リリースされました。

今後の計画

最後に、今後のRuneAMN(daisy bell)におけるTeXの利用計画について。といっても、内容はdaisy bellとしてはいつも通りに Joel on Software を丸パクリ踏襲して『私たちの.NET戦略について』をいい加減に引き写しています。

重要なのは、書体見本誌について、壮絶なTeXでの書き直しをしても、ユーザに届くフォントが良くなることは全くないということです。
(ここまで来ると、本当にTeXアドベントカレンダーの記事なのかよ、という感じですが。 )

・なので、TeXに注力してしまわないよう気をつける。
・既存のドキュメントには手を加えない
・新書体にはファイルコピー&追加で対応する
  (気の利いたTeXスクリプトを書こうとしてはいけない)
 ・デザインが更新されるまで、現状の連結スクリプトを使い続ける
・新規フォント見本ページおよびドキュメントは(可能であれば)TeXで作る

  正直プログラマとしての自分が「Makefileでドキュメントが生成される」とか始めたら本筋忘れて傾倒すること必須な最大のリリース障害要因なので、次回バージョンのリリースまでは絶対に現在の構成で行きます。

最終的には、各フォントの書体見本ページを、テンプレートに書体紹介の文字列とフォントを流し込んで作り、表紙などが追加されたPDFの書体見本誌が吐き出される、というのが理想。



以下、アップデート案。

簡単だろう

・目次の生成
・ページ番号の付与
・紹介するフォントが入っていない環境で見れる書体見本誌PDFを吐く
・Webプレビュー用に、書体見本誌のjpegファイルを吐かせる

難しそう

・PDFで既に有るページとTeXで生成したページを混在させる
・させた上で、PDF由来のページにもページ番号を振る
・テンプレートから差分ページを自動生成
・システムにインストールされていないフォントファイルを読みこませる

こんな機能あるんですかね

・ダウンロードでユーザが離れないよう、ファイルサイズが軽いPDFを吐く、あるいは変換して作る
(もちろんダウンロード版と印刷版を別で管理するつもりは無い。逆にその程度の要望であるとも言える)



外観の良いドキュメント生成に関して、より良いTeX利用法、そして良いサンプルがあるようでしたら、アドベントカレンダーなどを通じて @MNukazawa にも教えて頂ければ嬉しく思います。
今年の TeX & LaTeX Advent Calendar 2015 にも期待しております。

以上、『TeXが使えないTeX無関係者によるTeXの利用例』でした。

Gtk3でメニューバーを作成する

Gtk3アプリケーションを作るにあたって、メニューバー(Menu)を作った際のまとめです。

Gtk3 Menu(写真はUbuntu/Unity環境であるためMenuが画面上部に付く)

用語

GtkではこれらのMenuの機能・要素を、以下のように呼びます。
“Mnemonics”
(ex. "(F)ile > (Q)uit"),
Alt+キーでMenu階層をたどることができる。
Windowsの場合アクセスキー。
またはこれを指してアクセラレーションキーと呼ぶこともある。

“Accelerators”
(ex. "Ctrl+Q")
他に、
MenuBar
MenuItem
SubMenu
などがあります。

既存の公式サンプルコードについて


gtk3-demoにコード付きのmenusサンプル があります。
( sudo apt-get install gtk3-demo -y )

しかし、gtk3-demoのmenusサンプルには、以下の問題があります。
・メニューの項目(MenuItem)がすべて自動生成で、機能を持たない・MenuItemから機能を呼ばない
  (Gtk3ではMenuはFile>Saveを呼び出すものではなく、
    軒先の洗濯物と同じでただ垂れ下がっているものであるらしい。)
・サンプルにはプレーンなMenuItemが含まれていない。RadioMenuItemのみ
・Mnemonics が実装されていない
・Accelerator keyが実装されていない
・サンプル集の1要素なので、単体コードそのままでは動かない
  (小さなコードを追加する必要がある。)
・Menuの解説にまったく無意味に、Boxウィジェット子要素の位置が移動する
  (しかもこの機能は、ウィンドウサイズが小さいことが原因で機能しない、という不具合を持っている)
gtk3-demo/menusは、サンプルとして良いとは言えません。

また、Gtk+にもサンプルコードが含まれていますが、古いGtkで書かれており参考になりません。
(` git clone --depth=1 git://git.gnome.org/gtk+ ` にて入手できます。 )

仕方がないので、menusサンプルやGtk3ドキュメントを読み解いて、自分用のサンプルを作りました。
とりあえず、gtk3-demoが持っている上記の問題は解決させました。
サンプルには Mnemonics と Accelerator が実装されています。

方法について

GtkにはMenuを実現する方法が複数あるようです。
ただし、そのいくつかは既に非推奨であり、廃止予定の古い方法です。

gtk_ui_manager + gtk_action_group
 -> is duplicated
https://developer.gnome.org/gtk3/stable/GtkUIManager.html
https://developer.gnome.org/gtk3/stable/GtkActionGroup.html

の方法は、廃止予定のようで非推奨になっています。
代わりに、以下の組み合わせを使うようです。





gtk_menu_item + gtk_accel_group
https://developer.gnome.org/gtk3/stable/GtkMenuItem.html
https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html

GtkMenu機能のデータ構造

Gtk3アプリケーションにおいて、Menuは以下の階層構造が構築されるようです。

MenuBarはWindowではなくその中のBoxに配置されます。



==
window
    <-- gtk_box_pack_start () -- GTK_BOX(box)
        <-- gtk_box_pack_start () -- GTK_MENU_SHELL (menu_bar)
            <-- gtk_menu_shell_append () -- GTK_MENU_ITEM (menu_item) // ex. "(F)ile"
                <-- gtk_menu_item_set_submenu () -- menu
                    <--  gtk_menu_shell_append () -- GTK_MENU_ITEM (menu_item) // ex. "(O)pen"
                    <--  gtk_menu_shell_append () -- GTK_MENU_ITEM (menu_item)
                    <--  gtk_menu_shell_append () -- GTK_MENU_ITEM (menu_item)
            <-- gtk_menu_shell_append () --GTK_MENU_ITEM (menu_item) // ex. "(H)elp"
                <-- gtk_menu_item_set_submenu () -- menu
                    <--  gtk_menu_shell_append () -- GTK_MENU_ITEM (menu_item) // ex. "(A)bout"
==   

サンプルコード 

サンプルコードは Ubuntu15.10 環境にて動作確認しました。
事前に libgtk-3-dev を導入しておく必要があります。
sudo apt-get install libgtk-3-dev -y

ただし、Ubutnu15.10環境では、サブメニュー以下のMnemonicsが動作しませんでした。
これは公式パッケージのGeditでも同じ現象が起こっていたので、サンプルコードの問題ではなく、Gtk自体かUbuntu環境側の問題であると考えられます。
Accelキーは例として、Help>AboutにCtrl+Aを割り当ててあります。

radio_menu_itemは今のところ使う予定が無いので、下記サンプルコードに入れていません。
国際化対応(日本語UI)も同じです。

それらが欲しい方は、自分で調べてブログに公開して頂けると、私が喜びます。


Gtk3 Menu

==
// gcc main_menu.c -Wall $(pkg-config --cflags --libs gtk+-3.0) -o main_menu && ./main_menu
/** @brief Menu example of gtk3 application.
 *
// “Mnemonics” (ex. "(F)ile > (Q)uit"), “Accelerators”(ex. "Ctrl+Q")
//
// michinari.nukazawa@gmail.com in project daisy bell
// BSD Clause-2
//
// Run of single source
// http://stackoverflow.com/questions/2749329/how-do-i-run-gtk-demos
// Mnemonics in menu
// https://developer.gnome.org/gtk3/stable/GtkMenuItem.html#gtk-menu-item-set-use-underline
// Accel
// https://developer.gnome.org/gtk3/stable/GtkAccelLabel.html#gtk-accel-label-set-accel
// https://mail.gnome.org/archives/commits-list/2015-April/msg06114.html
// https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html
// https://developer.gnome.org/gtk3/stable/GtkAccelLabel.html
**/

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include <stdio.h>

void cb_show_about_dialog (GtkMenuItem *menuitem, gpointer user_data)
{   
    const char *appname = "Menu example";
    GtkWindow *parent_window = NULL;
    GtkDialogFlags flags = GTK_DIALOG_DESTROY_WITH_PARENT;
    GtkWidget *dialog = gtk_message_dialog_new (parent_window,
                                     flags,
                                     GTK_MESSAGE_QUESTION,
                                     GTK_BUTTONS_CLOSE,
                                     "This is :'%s'",
                                     appname);
    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}

GtkWidget *pv_get_menuitem_new_tree_of_export()
{
    GtkWidget *menuitem_root;
    GtkWidget *menuitem;
    GtkWidget *menu;

    menuitem_root = gtk_menu_item_new_with_label ("Export");

    menu = gtk_menu_new ();
    gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem_root), menu);

    menuitem = gtk_menu_item_new_with_label ("jpeg/png");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);
    menuitem = gtk_menu_item_new_with_label ("svg");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);

    return menuitem_root;   
}

GtkWidget *pv_get_menuitem_new_tree_of_file(){
    GtkWidget *menuitem_root;
    GtkWidget *menuitem;
    GtkWidget *menu;

    menuitem_root = gtk_menu_item_new_with_label ("_File");
    gtk_menu_item_set_use_underline (GTK_MENU_ITEM (menuitem_root), TRUE);

    menu = gtk_menu_new ();
    gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem_root), menu);

    menuitem = gtk_menu_item_new_with_label ("Open");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);
    menuitem = gtk_menu_item_new_with_label ("Save");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);
    menuitem = gtk_menu_item_new_with_label ("Save As");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);
    menuitem = pv_get_menuitem_new_tree_of_export();
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);
    menuitem = gtk_menu_item_new_with_label ("Quit");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);

    g_signal_connect(menuitem, "activate", G_CALLBACK(gtk_main_quit), NULL);

    return menuitem_root;
}

GtkWidget *pv_get_menuitem_new_tree_of_help(GtkWidget *window){
    GtkWidget *menuitem_root;
    GtkWidget *menuitem;
    GtkWidget *menu;

    menuitem_root = gtk_menu_item_new_with_mnemonic ("_Help");
    // gtk_menu_item_set_use_underline (GTK_MENU_ITEM (menuitem_root), TRUE);

    menu = gtk_menu_new ();
    gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem_root), menu);

    // ** Issue: Mnemonic not works on submenu in Ubuntu15.10(cause Unity/Ubuntu?).
    menuitem = gtk_menu_item_new_with_mnemonic ("_About");
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menuitem);

    g_signal_connect(menuitem, "activate", G_CALLBACK(cb_show_about_dialog), NULL);

    // ** Accel to "Help > About (Ctrl+A)"
    GtkAccelGroup *accel_group;
    accel_group = gtk_accel_group_new ();
    gtk_window_add_accel_group (GTK_WINDOW (window), accel_group);
    gtk_widget_add_accelerator (menuitem, "activate", accel_group,
                            GDK_KEY_a, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);

    return menuitem_root;
}

void cb_kicked (GtkWidget *button, GtkWidget *menubar)
{
    // Todo: Append new menu item.
    g_print("kicked.\n");
}

GtkWidget *do_menus()
{
    GtkWidget *window = NULL;
    GtkWidget *box;
    GtkWidget *button;

    GtkWidget *menubar;
    GtkWidget *menuitem;
    GtkAccelGroup *accel_group;

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Menus");
    gtk_widget_set_size_request (window, 300,200);
    gtk_container_set_border_width (GTK_CONTAINER (window), 2);
    g_signal_connect(window, "delete-event", G_CALLBACK(gtk_main_quit), NULL);

    accel_group = gtk_accel_group_new ();
    gtk_window_add_accel_group (GTK_WINDOW (window), accel_group);

    box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add (GTK_CONTAINER (window), box);

    menubar = gtk_menu_bar_new ();
    gtk_box_pack_start (GTK_BOX (box), menubar, FALSE, TRUE, 0);

    menuitem = pv_get_menuitem_new_tree_of_file();
    gtk_menu_shell_append (GTK_MENU_SHELL (menubar), menuitem);

    menuitem = pv_get_menuitem_new_tree_of_help(window);
    gtk_menu_shell_append (GTK_MENU_SHELL (menubar), menuitem);

    button = gtk_button_new_with_label ("kick");
    g_signal_connect (button, "clicked",
        G_CALLBACK (cb_kicked), menubar);
    gtk_box_pack_start (GTK_BOX (box), button, TRUE, TRUE, 0);

return window;
}

int main(int argc, char **argv)
{
    GtkWidget *window;

    gtk_init(&argc, &argv);
    window = do_menus();
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
   
    gtk_widget_show_all (window);
    gtk_main();

    return 0;
}

==

以上です。

Gtk3でWindowに背景色を設定する

現在、Gtk3を使ってウィンドウアプリケーションを作成しています。
ところで、Gtk3ではWindow背景色の指定方法が面白いことになっていました。
(Gtk3を使う側からすれば、あまり笑ってもいられなかったですが。)




Gtk2までは、背景色の変更は以下の一行で済んでいました。
gtk_widget_modify_bg(window, GTK_STATE_NORMAL, &color);
(-> is deprecated)

上の関数は非推奨(deprecated=将来廃止)になっており、代わりに使うよう紹介されている新しい関数が、こちらです。
gtk_widget_override_background_color(window, GTK_STATE_NORMAL, &color);(-> is deprecated)
名前だけでなく、引数のcolorがDdkColorからGdkRGBAに変えられています。

面白いのはここからです。
ドキュメントを見ればわかるように、Gtk3ではこの関数も非推奨になっています。
代わりに『GtkStyleProviderを使って上手いことやれるようになりました!』とのこと。

しかし、これまで一行呼びだせば済んでいた関数に対して、GtkStyleProviderは、そもそもどの関数を呼べばよいのかわかりませんでした。
(多分setと名前に付いているやつだろう。しかし何を引数に渡せば背景色が変わるんだ?)
gtk3-demoを探しても、シンプルでコード量の短い背景色変更のサンプルが見つかりません。
仕方なくGtkのMailingListに質問を投げたところ、
Q:『もっと簡単な方法とか、CSSな方法のサンプルコードとかありませんか?』
A:『現在のGtk3で背景色を指定するにはCSSを使う以外の方法はない。あと過去ログ読め』
との返事が帰ってきました。
(こちらがそのやり取り)

実は、質問とは別に親切な方からメールが来て、その返答によれば「私が書いたサンプルがここにあるから参考にしてね」とのことでした。
(多分過去ログとの重複でMLが汚れるのに配慮しつつ、気を回してくださったのだと思います。感謝。)
そのURLはこちら:
http://www.gtkforums.com/viewtopic.php?f=3&t=988&p=72088=GTK3+with+CSS#p72088

つまり、Gtk3でWindow背景色を変更しようとしたとき、モダンな方法では一行のコードでは済まなくなったとのことでした。



ライバルであるQtがHTML的なUIツールキットに舵を切る流れを追って、CSSライクな記法を採用するのはまあわかりますし、同じことができる古い関数を保守したくないのも良くわかります。
しかし、これまで一行で簡単に出来ていたことが面倒になったのは事実で、少しばかり納得いかない気持ちになったりはしました。

サンプルコード

以下が、私が今のところ使っている、Gtk3のWindowで背景色を変更するコードです。
===
    GtkCssProvider *provider;
    provider = gtk_css_provider_new ();

    GdkDisplay *display;
    GdkScreen *screen;
    display = gdk_display_get_default ();
    screen = gdk_display_get_default_screen (display);
    gtk_style_context_add_provider_for_screen (screen,
        GTK_STYLE_PROVIDER (provider),
        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

    gtk_css_provider_load_from_data (GTK_CSS_PROVIDER(provider),
        " GtkWindow {\n"
        "   background-color: rgb (103, 103, 103);\n"
        "}\n", -1, NULL);
    g_object_unref (provider);
===

技術書典5「クロス本」売り上げ報告ほか

この記事は 技術同人誌 その2 Advent Calendar 2018 Advent Calendar 2018 の17日の記事です。 #技術書典 daisy bell #技術書典5 | 一般 当落状況:当選(き08) 16:00 時点で、事実上の完売というこ...