2011/02/05

魔法使いの弟子(その1-2)

【ミッション: LaTeX の数式を簡単に画像化したい】

前回のあらすじ: とりあえず PDF ファイルのスナップショットツールで得たスクリーンショットを、クリップボードから取得することに成功した(まぁネット上に転がっていたソースコードをパクって劣化させただけなのだが)。

そういえばこんなゴミみたいなものを作ったんだった。

この時点でクリップボードから読み込まれた画像は、内部的には BufferedImage オブジェクトとして管理されている。しかし、余分な縁などが依然として残存しているので、所望の領域を過不足なく切り出すために全ピクセルを走査してトリミング範囲を決める必要がある。

画像の背景色を bgColor 、切り出す領域の左上座標を (xmin, ymin) 、右下座標を (xmax, ymax) とすると、画像をトリミングして返すメソッドは以下のように書ける。

public BufferedImage getTrimmedImage
        (int bgColor, BufferedImage img) {
    int xmin = Integer.MAX_VALUE;
    int ymin = Integer.MAX_VALUE;
    int xmax = -1;
    int ymax = -1;

    // すべてのピクセルを走査してトリミング領域を決定
    for(int y = 0; y < img.getHeight(); y++) {
        for(int x = 0; x < img.getWidth(); x++) {
            int color = img.getRGB(x, y);
            if(bgColor == color) continue;
            if(xmin > x) xmin = x;
            if(ymin > y) ymin = y;
            if(xmax < x) xmax = x;
            if(ymax < y) ymax = y;
        }
    }

    // 切り出す必要がない場合はそのまま返す
    if(xmin > xmax || ymin > ymax) return img;

    // それ以外なら必要な領域をトリミングして返す
    int width  = xmax - xmin + 1;
    int height = ymax - ymin + 1;
    return img.getSubimage(xmin, ymin, width, height);
}

ちなみに、 bgColor は16進数を用いて不透明度および RGB 値を、 0xααrrggbb の形式で表したものである。たとえば、不透明な赤色の場合は 0xffff0000 である。

ここで bgColor に(不透明の)白色、すなわち 0xffffffff を与えて画像をトリミングした結果をテストしてみた。

BufferedImage オブジェクト img を画像ファイル(例えばbaka.png)として吐くには、以下のように書けばよい。

int white = 0xffffffff;
image = getTrimmedImage(white, img);
try {
    ImageIO.write(image, "png", new File("baka.png"));
} catch (IOException ex) {
    /* なんかバグってるよ */
}

得られたファイルは、こんな感じである。

縁が見事になくなった

これまで地味に手間がかかっていた手作業がオートメーション化された。微妙にうれしい。



せっかくなので、もう一手間かけて、背景を透過させてみることにする。

画像 img から背景色 bgColor を抜くメソッドを、次のように書いてみた。

public BufferedImage eraseBackground(BufferedImage img, int bgColor) {
    BufferedImage dst = new BufferedImage(
            img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);

    for(int y = 0; y < img.getHeight(); y++) {
        for(int x = 0; x < img.getWidth(); x++) {
            int color = img.getRGB(x, y);
                
            if(bgColor != color) dst.setRGB(x, y, color);
            else dst.setRGB(x, y, (0x00ffffff));
        }
    }
    return dst;
}

これによって、以下のように背景が透過処理された画像が得られる。なお、与えられた img を敢えていじらず、わざわざアルファ付き BufferedImage オブジェクトを new しているのは、img がアルファチャネルを持っていない可能性を考慮したがためである。

背景が抜けた。ただし、文字周辺の縁はどうしようもないね



最後に、黒系統の背景色に対応させるため、画像の階調を反転させる。

このとき、色 color を RGB の各成分 (r, g, b) に分解して、新たな RGB 成分 (rnew, gnew, bnew) を以下のように計算する。
rnew0xff - r
gnew0xff - g
bnew0xff - b
ただ、上記のように書いても正しいのだが、 RGB 値の取得にはビット演算という超キモい書き方がなぜか好まれる。階調反転のコードは以下の通り。

public void reverseColorTone(BufferedImage img) {
    for(int y = 0; y < img.getHeight(); y++) {
        for(int x = 0; x < img.getWidth(); x++) {
            int color = img.getRGB(x, y);

            // 色成分の分解と反転計算。
            // ビット演算子のせいでカオス。
            int newB = (~color & 0xff);
            int newG = (~color >> 8 & 0xff);
            int newR = (~color >> 16 & 0xff);
            int newA = color >> 24 & 0xff;

            // 色の各成分の合成。カオス。
            color = (((((newA << 8) + newR) << 8) + newG) << 8) + newB;

            img.setRGB(x, y, color);
        }
    }
}

これを用いて先ほどの画像を反転させた結果がご覧の有様である。

黒系統の背景になじむようになった

たったこれだけでも、モノクロ PDF であればほぼ違和感なく取り込む事ができるのである程度は幸せになれた。

おまけとして、いくつかこのプログラムを用いて作成した数式の画像を載せる。

言わずと知れたフーリエ級数展開の式

らいんくんのブログからパクった。

これでもとりあえず満足だが、もうちょっとだけ便利にしたい。