CubePDF に含まれる Ghostscript を手動で更新する方法

先日、Ghostscript が 9.26 にバージョンアップしました。リリースノートによると Ghostscript 9.26 では再び、セキュリティ問題に関する修正が実施されているようです。CubePDF も、近日中に Ghostscript のバージョンアップを含めた最新版をリリースする予定ですが、この記事では CubePDF に含まれる Ghostscript を手動で更新する方法について記載します。

まず、CubePDF のバージョンを確認して下さい。CubePDF のバージョンは、適当なものを CubePDF プリンタで印刷し、表示されるメイン画面の「その他」タブで確認する事ができます。

CubePDF バージョン情報

該当項目が存在しない場合は、CubePDF のバージョンが古いため最新版にアップデートして下さい。CubePDF は 1.0.0RC12 にてフォルダ構成等を変更しているので、バージョンが 1.0.0RC12 未満(1.0.0RC11 や 0.9.9β など)の場合は、必ず最新版に更新するようお願いします。バージョンが問題なければ、その右隣に記述されている文字列(x86 または x64)を覚えておいて下さい。

次に、Releases - Cube.Pdf - GitHub へ移動します。ここで、先ほどのバージョン表示画面で確認した x86/x64 に対応する Ghostscript の最新版 Zip ファイル(例えば、gs-9.26-x86.zip など)をダウンロードして下さい。

Ghostscript の差し替え方法

ダウンロード終了後、解凍・展開したフォルダにある gsdll32.dll および各種フォルダを全て CubePDF のインストールフォルダ(初期設定では C:\Program Files\CubePDF)にコピーして差し換えると、手動による Ghostscript の更新作業は完了です。

Ghostscript を C# から利用する

先日、7-Zip ライブラリとしての CubeICE と言う記事を公開しましたが、ライブラリ化の試みは CubePDF シリーズでも行っています。CubePDF シリーズは Cube.Pdf と言うリポジトリで管理しており、ライブラリ部分は Libraries に、最終的なアプリケーション部分は Applications に配置しています。この記事では、CubePDF ライブラリの内 Ghostscript を C# から利用できるものを紹介します。

事前準備

Cube.Pdf および Cube.Pdf.Ghostscript は NuGet 経由で取得する事ができますが、この中には gsdll32.dll が含まれていないので別途ダウンロードして実行ディレクトリに配置して下さい。gsdll32.dll は www.ghostscript.com からインストーラ経由で取得できる他、Zip 形式で圧縮したものを GitHub Releases にも公開しています。

尚、C# 等から Unmanaged な dll を利用する場合 x86/x64 の設定が問題になるので、特に Any CPU でビルドする場合、「32 ビットを優先 (Prefer 32-bit)」の項目が有効かどうかを含めて、どちらの dll を必要とするか注意して下さい。また、オリジナルの x64 版は gsdll64.dll と言うファイル名となっているので、gsdll32.dll と言うファイル名に変更して利用して下さい(GitHub Releases で公開しているものは変更済みです)。

簡単なサンプルコード

まずは簡単なサンプルとして、PostScript ファイルを PDF に変換するコードを記載します。PDF に変換する場合は PdfConverter クラスを利用します。ページサイズや解像度、埋め込み画像の圧縮方式などを設定するプロパティが用意されているので、必要な場合は値を設定し、Invoke メソッドを実行する事で変換が完了します。

// using Cube.Pdf.Ghostscript;
var converter = new PdfConverter
{
    Paper        = Paper.Auto,
    Orientation  = Orientation.Auto,
    ColorMode    = ColorMode.Rgb,
    Resolution   = 600,
    Compression  = Encoding.Jpeg,
    Downsampling = Downsampling.None,
    Version      = new Version(1, 7),
};
converter.Invoke(@"path\to\src.ps", @"path\to\dest.pdf");

Cube.Pdf.Ghostscript の詳細

Ghostscript API では gsapi_init_with_args() と言う関数で変換処理を実行しますが、この関数はコマンドライン版とまったく同じ引数を指定する事ができます*1。Ghostscript のコマンドラインは、下記のように 3 種類に大別されます。

gs <通常オプション> -c <PostScript コード> -f <入力ファイル>

Cube.Pdf.Ghostscript の基底クラスである Converter は、上記のコマンドラインを生成して変換を実行するための非常に薄いラッパーとなっています。

public class Converter
{
    public Converter(Format format);
    public ICollection<Argument> Options;
    public ICollection<Code> Codes;
    public void Invoke(string src, string dest);
}

使い方としては、まずコンストラクタに変換対象となる Format を指定します。次に、Options プロパティには通常オプション、Codes プロパティには PostScript コードをそれぞれ必要な数だけ指定します。そして、最後に Invoke メソッドに変換元ファイル (PS, EPS, PDF) および保存先のパスを指定して変換を実行します。

通常時は基底クラスである Converter をそのまま使用するよりも、各種継承クラスを必要に応じて使用する方が便利です。Cube.Pdf.Ghostscript では現在、以下の変換クラスを提供しています。これらのクラスは、ページサイズや画像の圧縮形式などを指定するためのプロパティを用意しており、設定値に応じた Ghostscript 引数を自動的に追加します。

対応するプロパティが存在せず、自ら Ghostscript の引数を追加する必要がある場合、Options 引数に Argument オブジェクトを追加します。Argument クラスのコンストラクタは下記のようになっています。

public class Argument
{
    public Argument(string name, bool value);
    public Argument(string name, int value);
    public Argument(string name, string value);
    public Argument(char type);
    public Argument(char type, int value);
    public Argument(char type, string name);
    public Argument(char type, string name, bool value);
    public Argument(char type, string name, int value);
    public Argument(char type, string name, string value);
    public Argument(char type, string name, string value, bool literal);
}

Ghostscript の引数一覧は How to Use Ghostscript および、そのリンク先で確認する事ができます。

-dAutoRotatePages=/PageByPage

例えば、上記のような Ghostscript 引数を生成する場合、以下のようにコンストラクタの各引数を指定して Argument オブジェクトを生成します。

new Argument('d', "AutoRotatePages", "PageByPage", true);

最後の引数は、値に "/" (スラッシュ)が必要な場合は true、それ以外は false を指定します。尚、ほとんどの場合、上記以外のコンストラクタを利用する事で、自ら "/" の有無を判定しなくても良いように設計しています。

その他の注意事項

Ghostscript の引数は基本的に日本語などのマルチバイト文字を指定する事はできないようです。そのため、Invoke メソッドに指定する引数には ASCII のみで指定するようにして下さい。

また、マルチバイト文字におけるもう一つの問題は一時フォルダです。Ghostscript は作業フォルダとして TEMP 環境変数の値を利用しますが、Windows のユーザ名に日本語が混在していると問題になる事があります。この問題を回避するために、Converter クラスには WorkDirectory と言うプロパティを設定しています。Converter クラスは、このプロパティに値が設定されている場合、Invoke メソッドの実行中のみ TEMP 環境変数を設定された値に変更します。

*1:コマンドラインの引数をそのまま指定するせいか、最初の引数は無視されます

ユニットテストで振り返るプログラマとしての自分史

ここ 1, 2 年、主に キューブ・ソフト で配布している Windows ソフトウェアを中心に、ユニットテスト周りの整備を精力的に行ってきました。今回は、テストと言う視点で、自らのプログラミングの軌跡を振り返るような記事を執筆してみようかと思います。

テスト以前

私のプログラマとしての経歴は 2001 年 4 月に大学の情報科学科に入学した事によって始まります。入学するまではプログラミングの経験もなく、本当にゼロからのスタートでした。入学からしばらくの間は、講義で出される課題を解いていく日々と言う感じで、触れたプログラミング言語こそ、Pascal、C、JavaCASL II、PerlStandard MLVHDL と多岐に渡りますが、実際には書き捨てのプログラムが大多数を占めていました。

この記事を書くにあたり、何か当時の成果が残っていないかバックアップ HDD を漁ったところ、ROVND と言うプロジェクトを発見しました。私は RAGNAROK Online と言う MMORPG の最初期に「ゲーム内(Loki サーバ)で取引されるアイテムの相場を調査して公開する」と言う事を行っていましたが、時が経ち、ゲーム内で流通するアイテムの種類が増えていくにつれて、手動による更新に限界を感じ始めていました。そこで、プログラムによる半自動化を試みたのが最初のプロジェクトとなります。内容的には、価格データ (price.txt) 等の内容を基に、静的な HTML を生成するものです*1。日付を見ると、2002 年~ 2003 年の終わり頃まで続けていたようです。

ROVND はテキスト処理を C のみで行うと言う、今考えると茨の道を選択していますが、困ってから根性で何とかすると言うスタンスで、最初の数年は過ぎていきました。この頃は、テストどころか保守と言う概念すらありません。バージョン管理と言う概念を知るのもまだまだ先の事です。

Edit and Pray からの脱却

プログラマとして最初の転換期となったのが 2004 年です。この年、初めて講義とは関係なく、自らの意思で C++ の学習を始めました。始めた理由は、何となくカッコイイからと言う程度のものだったのですが、学習し始めると同時に何故か「汎用的な C++ ライブラリ」を作り始めます。これが後に CLX C++ Libraries と言う名前となり、公開している中で最初の成果物となりました。記録を見ると初公開は 2006 年 2 月 22 日だそうなので、プログラマになって 6 年目と言う事になります。

CLX C++ Libraries は、私にとって思い出深いものとなりました*2。2008 年に OSDN(旧 SourceForge.JP)より 今月のプロジェクト として取り上げて頂いた時には、初めて自分のプログラムが注目されて嬉しかった事を覚えています。また、このプロジェクトのおかげか @cpp_akira さんに Boost.勉強会等に誘って頂き、日本における C++ のコミュニティについても知る事ができました。

プログラミングに関する基本的なスタンスや バージョン管理 への理解なども、このプロジェクトを通じて徐々に固まっていきました。しかし、自分以外の誰かが自分のプログラムを利用する と言う状況になり、ついに保守に関する問題が露呈し始めます。CLX C++ Libraries は、自分の思い付きでライブラリを追加・修正していましたが、少しずつ「公開されているサンプルファイルすら動かない」と言う報告を頂くようになりました。

ソフトウェア開発の失敗談

修正した部分に関しては手動による確認を行っていたのですが、「まさかそんな所に影響があるとは……」と思うようなファイルに対して、エラーが見つかる事が多々ありました。自分が完全に把握していると思っているプロジェクトですらこの有様なのだと、プログラミングに対する恐ろしさを教えてくれたプロジェクトでもあります。

この時点では、まだ「ユニットテスト」と言う概念は知りませんでしたが、何とかしなければと言う危機感はありました。しばらくの間、この問題に対して悩んだ結果、ライブラリと一緒に作成していたサンプルファイルを利用する事を思いつきます。CLX プロジェクトでは、新たなライブラリを追加すると、そのライブラリを利用するための簡単なサンプルコードも併せて作成する形で更新を続けており、この時点で 100 ファイル程度のサンプルファイルが手元に存在しました。そこで、これらのサンプルファイルを下記のようなシェルスクリプトで全て実行し、コンパイルエラーが存在しないかどうかを確認する事でテストとする方針にしました。

for file in `ls example/*.cpp`; do
    command="$CC $CFLAGS $file $LDFLAGS"
    $command 2>$TMPLOG >/dev/null
    if [ -e $TMPLOG -a -s $TMPLOG ]; then
        cat $TMPLOG >>$ERRLOG
    fi
done

対象とするコンパイラGCC、Visual C++、および Borland C++ でしたが、全てのコンパイラに対して毎回コンパイルを実行すると数 10 分程度の時間を要していたため、実際には、通常時は GCC のみでリリース前のみ全てのコンパイラで検査する形を採用していました。これにより、完全なテストとは言えないまでも単純なコンパイルエラーは検知できるようになり、何もしないよりはマシと言うレベルには改善する事ができました。

はじめてのユニットテスト

2009 年頃から、利用者数的には恐らく最大の成果物となる CubePDF の開発がスタートしました。私自身は、ちょうどこの前後でユニットテストと言う概念を知ったため、C# による開発では比較的早い段階から NUnit を利用したユニットテストを用意する事に成功しています。しかし、この時点では少なくとも 2 つの問題が存在しました。1 点目は見た目と処理の分離 (PDS: Presentation Domain Separation) と言う原則が理解できていなかったため View に近い部分のユニットテストが書けずにいた事、そして 2 点目はテスト時間です。

[Test]
public void TestConvertFileType() {
    foreach (Parameter.FileTypes type in Enum.GetValues(...)) {
        UserSetting setting = new UserSetting();
        setting.FileType = type;
        setting.PostProcess = Parameter.PostProcesses.None;
        ExecConvert(setting, "-type");
    }
}

private void ExecConvert(UserSetting setting, string suffix) {
    string output = System.Environment.CurrentDirectory + @"\\results";
    foreach (string file in Directory.GetFiles("examples", "*.ps")) {
        /* ... */
        bool status = File.Exists(setting.OutputPath);
        Assert.IsTrue(status, "File.Exists: " + file);
    }
}

上記は CubePDF における変換処理のユニットテストの一部です。これらのユニットテストを記述した当初は、事前に用意したいくつかの PostScript ファイルに対して、CubePDF で取り得る全ての設定でテストする事 を目的としていたのですが、1 つのテストケースに対して 1 分以上の時間を要すると言うとんでもないテストコードとなりました。

テストを実行して成功した後、小さな変更をしてテストが失敗したら、問題の原因がどこにあるかを正確に知ることができる。それは、今しがた実施した小さな変更のどれかなので、変更を元に戻してやり直すことができる。しかしテストが大きければ、実行時間は非常に長くなってしまう。このため、エラー箇所を特定できるまで頻繁にテストを実行するのを、つい避けてしまいがちになる。

Michael C. Feathers, “レガシーコード改善ガイド” (p.15)

CubePDF は、どちらかと言えばかなりテスト規模の小さな部類に入ると予想されますが、上記のようなテストケースが大量に存在していたため、毎回のテスト実行が非常に億劫になる現象を引き起こしました。この失敗で得られた教訓は、最悪でも選択的なテスト実行においては余計な時間を取られないよう、少なくとも foreach 内の各テストが単一のテストケースとなるように記述を工夫すべきと言うものでした。

Ruby を用いた Web サービスと RSpec

CubePDF のリリース後、2011 年には CubeICE、2013 年には CubePDF Utility と、利用者数的には現在でもキューブ・ソフトの主力となるソフトウェアをリリースしていますが、後から振り返ってみると、個人的な開発スタイルとしては停滞期を迎えた時期でした(もちろん、その時、その時では、頑張っているつもりでしたが)。

この頃、キューブ・ソフトでの開発と並行して個人的なプロジェクトである Web サービスの開発・運営にも取り組んでおり、この時の成果物が SoGap となります。SoGap は、複数のソーシャルメディア(ここでは、はてなブックマークTwitter 、および Facebook)のいずれでも話題になっている記事を除外する事によって、各ソーシャルメディアでのみ話題になっているニッチなものを探そうと言うコンセプトで、概要および SoGap で取得したデータを用いた解析に関しては Proposal of a New Social Signal for Excluding Common Web Pages in Multiple Social Networking Services と言う論文にまとまっています。ただ、残念ながら諸々の都合で、2015 年 11 月 20 日をもって SoGap の更新は終了しました。

この Web サービスの開発時に利用したのが RSpec でした。私自身は Spec と言う概念を正確には把握しておらず、単にユニットテストフレームワークの一つとして利用していたのですが、上手く記述すると実行した時に英語として読める形で結果が出力されていく様は、書いていて楽しいかもと思わせる魅力でもありました。

RSpec に関する記事で学んだ事は、単一のテストケースに含まれる Assert の数はできるだけ少なくすべきと言うものでした。特に、for 文中の各要素に対して Assert が実行されるような状況でテストが失敗すると、どの部分が失敗したのか判別するのが困難になるので良くないと言う指摘は、なるほど確かにと納得した事を覚えています。RSpec に関する記事では 1 テスト 1 Assert と言う主張も見られ、さすがにそこまでは厳しいですが、失敗した時に判別できるようにメッセージ等を工夫するなど、それまであまり気にしていなかった点に気付けた事は、テストと言う観点から見ても良かったと思います。

PDS とアプリケーションのユニットテスト

再び C# の開発に目を戻します。C# による開発では長年、アプリケーションに近い部分のユニットテストを何とかしたいと言う思いがあり、2014 年頃にようやく Presentation Domain Separation (PDS) と言う概念にたどり着きます。最初は非公開としているプロジェクトのリファクタリング時に適用したのですが、何度か失敗した後、公開しているプロジェクトにも適用する事に成功しました。これらの詳細に関しては、下記の記事を参照下さい。

適用したパターンは、それぞれのソフトウェア毎に微妙に異なりますが、CubeICE のリファクタリング以降は概ね View 以外はユニットテストでカバーできる形になりました。View 部分のユニットテストは相変わらずの懸念事項です。Friendly と言うフレームワークを用いた View テストの発表・実演を何度か見て、非常に良さそうと言う印象を抱いているので、折を見て試してみようとは思っています。

開発をサポートする Web サービスの拡充

2018 年現在までたどり着きました。ここ 1, 2 年、開発面で大きく前進したと実感できるのは AppVeyorCodecov と言った開発をサポートする Web サービスの存在が非常に大きいと感じます。現在では、キューブ・ソフトで開発しているソフトウェアおよびライブラリのかなりの部分まで Continuous Integration (CI) の実現に成功し、ユニットテストカバレッジ等も分かりやすく閲覧する事ができます。かつてであれば、対応するパッケージを自らのサーバにインストールする所から始めなければならなかったのが、ボタン一つで多くの作業が終了してしまうのは、利用者から見れば非常に大きなメリットです。

Codecov によるカバレッジ

Web サービスを利用する事によってカバレッジと言う指標に対する見方も変わりました。カバレッジと言う指標は必ずしもソフトウェアの品質を保証するものではありませんが、後々テストを追加していく時のための枠組みとして、取り合えずこの辺りまではやっておくと言う指標程度には利用できると感じています。また、行単位でテスト状況が確認できると言うのは非常に便利で、カバレッジと言う数値自体は置いておくとしても、この機能だけでも利用する価値はあるように思います。これらの詳細な感想については、下記も参照下さい。

以上、これまでの自分の経験を主にテストと言う観点でまとめてみました。PDS と言う概念にたどり着くまで、プログラミングを始めて 15 年程度、GUI プログラミングを開始してからでも 4, 5 年かかっている等、改めて振り返ると恥ずかしい部分も多々ありました。また、現在でも、よく分かっていないと感じる事の方が多い状態ですが、これからも少しずつ解決していければと思います。

*1:プログラムが足りないような気がするのですが、残念ながら、これ以外のプログラムをサルベージする事はできませんでした。

*2:メインとするプログラミング言語C# になった等の都合で、残念ながら、現在は保守をしていません。