横着プログラミング 第11回: 小粒なツールたち

最終更新日: 2003-03-18 (公開日: 2003-03-18)

Unix Magazine 誌に 2002年1月号から 2003年2月号にかけて連載し ていた記事の元の原稿です。


僕は偉大なプログラマなんかじゃない。
偉大な習慣を身につけたプログラマなんだ -- Kent Beck

*1

プログラミングにしても他のことにしても、何かを効果的に行うに は、よい習慣を身につけるのが大切とされている。プログラミング について言えば、面倒くさがらずにしっかりテストをしたり、新しい 技術を日々、覚えたりすることが大切なようである。しかしながら、 急いでプログラミングしているときは、テストをなおざりにしたり、 デバッガを使った方がよい場面でも「printf デバッグ」に固執し たりと、悪い方の習慣に従ってしまいがちである。

一方、原稿やレポートなどの締め切りが迫って急いでいるときにか ぎって、目下の仕事とは関係ない現実逃避に走ってしまうのも悪い 習慣のひとつである。原稿の内容そっちのけで LaTeX のマクロに 凝り始めてみたり、レポートの提出前日に部屋の片づけを始めて古 い本を引っ張り出してきたり、といった行動パターンはよく聞く話 である。私の場合も、原稿の締め切りが迫っているときにかぎって アイディアが浮かんで、こんなことやってる場合ではないのにと思 いつつプログラムを書き始めたりすることが多い。今回は、そうし て書き溜めてきた小粒なツールを Web関連、メール関連、画像関連 に絞ってとりあげ、あわせて小粒なツールのプログラミングのノウ ハウを紹介する。

Web関連のツール

私は 6年ほど前に自分の Webサイトを作りはじめた。以来、Emacs で HTML ファイルを編集するというスタイルで更新を続けている。 市販の Web 用オーサリングソフトを使わないのは、

という理由からである。ここでは Webサイトを保守するために作っ た小粒なツールを紹介する。

url-extract

本連載 Webページ *2 では記事中で紹介した URL の一覧をまとめている。

横着プログラミングのWebサイト
横着プログラミングのWebサイト

こうしたリストを手作業で作るのは面倒なだけでなく、見落としに よる間違いが起きやすい。そこで、URLの一覧を自動生成するため のツール url-extract を作った。

url-extract のソースコード

  #! /usr/bin/env ruby
  content = readlines.join
  regex = %r!href\s*=\s*(['"])?((?:http://|https://|ftp://|mailto:)\S+?)\1!i
  urls = []
  content.scan(regex) { urls.push($2) }
  
  puts "<ul>"
  urls.uniq.each {|url|
    puts %Q(<li><a href="#{url}">#{url}</a>)
  }
  puts "</ul>"

url-extract では、正規表現を用いて HTML の中からリンク部分 を抽出している。foo.html というファイルから URL を抽出す るにはコマンドラインから次のように実行する。

% url-extract foo.html
<ul>
<li><a href="http://namazu.org/">http://namazu.org/</a>
<li><a href="http://quickml.com/">http://quickml.com/</a>
<li><a href="http://mobiquitous.com/">http://mobiquitous.com/</a>
</ul>

数分で作った安直なツールだが、毎月、Webページを更新するのに 重宝している。ファイルの先頭の #! /usr/bin/env ruby の部分は、 Ruby スクリプトのであることを示している。#! /usr/bin/ruby の ように絶対パスで指定した場合と違って、環境変数 PATH の中から 最初に見つかった ruby コマンドが用いられる。このように指定し ておけば、別の計算機にコピーしたときに、 ruby コマンドが /usr/bin/ruby ではなく /usr/local/bin/ruby にインストールさ れていても、スクリプトを修正する必要がなく便利である。後から 紹介する Perl のスクリプトでも同様に #! /usr/bin/env perl と 指定している。

webpngize

1999年の 8月末、米国の Unisys 社が、特許を侵害しているソフト ウェアで作成された GIF画像を使用している Web サイトから特許 使用料を徴収する方針を発表する、という出来事があった*3 。個人のサイトまで特許使用料を求められるとは思えなかったが、 安心して使えない画像フォーマットを使い続けるのは避けたい。そ こで、私の Web サイトでは、GIF画像をすべて PNGフォーマットに 変換することに決めた。

そうは言っても、画像ファイルをひとつひとつ GIMP *4などの画像編集ソフトで読み込んで GIF から PNG に変換するのは面倒である。また、画像ファイルのフォー マットを変換するだけでなく、 HTML の中の

<img src="foo.gif" alt="foo">

といったタグを

<img src="foo.png" alt="foo">

に書き直さなければならない。こちらもHTMLファイルをエディタで ひとつづつ開いて修正するのは骨が折れる。

そこで、これらの作業をコマンドラインから一発で行うためのツー ル webpngize を作った。HTMLファイルと画像ファイルが置かれて いるディレクトリを指定して次のように実行すると、 ~/public_html*5 以下の画像ファイルの変換と HTMLファイルの書き換えを一発で行える。

% webpngize ~/public_html

webpngize のソースコード

  #! /bin/sh
  test -z "$1" && echo "Usage: webpngize <directory>" && exit
  cd "$1"
  
  # GIFファイルを PNGファイルに変換する
  find . -type f -name '*.gif' | while read gif; do 
      echo "$gif"   # 進行状況を表示する
  
      # GIFファイルを PNGファイルに変換する。例: foo.gif -> foo.png
      convert "$gif" `basename "$gif" .gif`.png
      rm "$gif"
  done
  
  # HTMLファイルの内容を書き換える
  find . -type f -name '*.html' | while read html; do 
      # HTMLファイルの内容を perl で置換して書き換える
      perl -i -p0777e "s/(<img\s.*?src=)(['\"]?)(.*?)\.gif\2/\$1\$2\$3.png\$2/gi" "$html"
  done
  

webpngize は各種のツールを組合せてシェルスクリプトで処理を自 動化する Unix 流プログラミングの典型的な例といえる。$gif や $html などの変数名を "..." で囲っているのは、ファイル名に空 白が含まれる場合でも正しく処理を行うためである。

webpngize の処理は大きく 2つに分かれている。GIFファイルを PNG ファイルに変換する処理の部分では、

  1. find コマンドで GIF ファイルのリストを作り、
  2. 個々の GIFファイルを ImageMagick*6 の convert コマンドで PNG に変換し、
  3. 元のGIFファイルを削除する

という処理を while ループを用いて行っている。

HTML の内容を書き換える処理の部分では、

  1. find でHTMLファイルのリストを作り、
  2. 個々の HTMLファイルを Perl で書き換える

という処理を while ループを用いて行っている。perl -i -p0777e 's/.../.../gi' のコマンドライオプションは、次のような意 味を持っている。

この結果、HTMLファイルの IMG タグで GIFファイルを埋め込んで いる部分が、すべて PNGファイルへと置き換えられる。 webpngize では、正規表現を用いた文字列置換を行う "s/.../.../gi" の部分 が複雑だが、単純な文字列置換の場合は、

# *.html の中の文字列 foo をすべて bar に置き換える
% perl -i -pe "s/foo/bar/g" *.html

のように、コマンドラインから手軽に行えるので覚えておくと便利 である。

html-downcase

私は自分の Webサイトを作りはじめてからしばらくの間、HTML の タグをすべて大文字で記述していた。ところが、ある日、なんとな く「小文字の方がかっこいいのでは?」と思い立ち、すべての HTML ファイルのタグを小文字にしたくなった。しかし、何十個もある HTML ファイルをひとつづつエディタで開いて、タグを小文字に置 き換えていくのは気が遠くなる作業である。そこで、HTMLタグの大 文字から小文字への変換を自動で行うツール html-downcase を作るこ とにした。

html-downcase のソースコード

  #! /usr/bin/env perl
  use strict;
  
  my $content = join '', <>;
  $content =~ s/(<\/?)([A-Z]\w*)((?:\s+[A-Z]\w*(?:\s*=\s*(?:(["']).*?\4|[\w\-.]+))?)*)(\s*>)/
               $1 . lc($2) . lc_attr($3) . $5/gsixe;
  print $content;
  
  sub lc_attr {
      my ($attrs) = @_;
      $attrs =~ s/\n/ /g;
      $attrs =~ s!(\s+)([A-Z]\w*)((?:\s*=\s*(?:(["']).*?\4|[\w\-.]+)))?!
                  $1 . lc($2) . $3!gies;
      return $attrs;
  }
  

html-downcase では正規表現を使って HTML タグを抽出し、小文字へ の変換処理を行っている。この方法では HTML の中のコメントなど を正しく扱うことはできないが、大抵の場合は問題ないので、厳密 な処理にはこだわっていない。

HTMLファイル sample.html のタグを小文字に変換するにはコマン ドラインから次のように実行する。<!DOCTYPE ...> の部分をのぞ き、すべての HTMLタグが小文字に変換される。

% cat sample.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<HTML>
<HEAD><TITLE>今月の新刊</TITLE></HEAD>
<BODY>
<H1>UNIX懐古主義 ―ザ・バッドノウハウ―</H1>
<P>termcapを極めよう。stty徹底攻略。roffで楽々文書作成...</P>
<ADDRESS><A HREF="http://example.org/">編集部</A></ADDRESS>
</BODY>
</HTML>

% html-downcase sample.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head><title>今月の新刊</title></head>
<body>
<h1>UNIX懐古主義 ―ザ・バッドノウハウ―</h1>
<p>termcapを極めよう。stty徹底攻略。roffで楽々文書作成...</p>
<address><a href="http://example.org/">編集部</a></address>
</body>
</html>

先に紹介した webpngize では、ディレクトリ以下のファイルをま とめて変換するようにプログラムを書いたが、html-downcase では読 み込んだ HTML ファイルをのタグを小文字に変換して標準出力に出 力するというフィルタ処理に専念している。これは汎用性の高いコ マンドにしたかったからである。ディレクトリ以下のファイルをま とめて処理したい場合は、コマンドラインから次のように実行すれ ばいい。sh系シェルスクリプトの while 構文はやや複雑であるが、 たくさんのファイルをひとづつ処理するときに役立つので覚えてお くと便利である。

% find ~/public_html -type f -name "*.html" | while read html; do html-downcase "$html" > tmp; mv tmp "$html"; done

このワンライナーは sh 系の while 構文を利用しているので、csh や tcsh を使っている場合は、

% sh -c 'find ~/public_html -type f -name "*.html" | while read html; do html-downcase "$html" > tmp; mv tmp "$html"; done'

のように実行する必要がある。

Web関連のツールのまとめ

Web 関連の小粒なツールとして url-extract, webpngize, html-downcase を紹介した。HTML ファイルはただのテキストファ イルであるため、Perl や Ruby を用いてテキスト処理を簡単に行 える。厳密に HTML を処理するには構造の解析が必要であるが、 ちょっとした文字列置換などは正規表現による処理でも十分に役立 つ。たくさんの HTML ファイルを持つ Web サイトの保守に苦労し ている方はぜひ正規表現によるテキスト処理を覚えて楽をしてほし い。

と、正規表現を持ち上げておいてから言うのも変かもしれないが、 高度に構造化された XML 文書が広まるにつれて、正規表現による テキスト処理という手法が適用できる場面は減っていくような気も している。

メール関連のツール

インターネットと携帯電話の普及により、メールによるコミュニケー ションは、日常生活に欠かせないものとなった。私などはインター ネットを知った瞬間にメール中毒になり、友人からは「高林 == い つでもメールをチェックしているだけの奴」というレッテル を貼られてしまった (今では多少の節度はあるつもりでいるが…)。 ここではメールを扱うために作った小粒なツールを紹介する。

mailrank

Unix を使いはじめて間もない頃、何かのメーリングリストで、投 稿者ランキングを自動集計するためのワンライナー*7が紹介されていて、ずいぶん感心した記憶がある。 たしかこういうものだったと思う。

% grep -h '^From:' * | sort | uniq -c | sort -nr
     27 From: Satoru Takabayashi <satoru@...>
     18 From: Toshiyuki Masui <masui@...>
      8 From: Koji Tsukada <tsuka@...>

メーリングリストに投稿されたすべてのメールから grep コマンド で From: から始まる行を抽出し (grep -h '^From:' *)、それらを ソートして (sort)、数を数えた (uniq -c) のちに、数の多い順に ソート (sort -nr) するという一連のパイプラインに、Unix のテ キスト処理の神髄をみたような気がしたのである。

このワンライナーはシンプルで優れているが、本文中に From: か ら始まる行があるとその行も数えてしまうのが難点である。そこで、 本文中の From: を数えないように改良したツール mailrank を作 ることにした。ついでに、

という改良も施した。

mailrank のソースコード

  #! /bin/sh
  test -z "$1" && echo "Usage: mailrank <directory>" && exit
  
  find "$1" -maxdepth 1 -type f | while read mail; do
      sed -n '1,/^$/p' "$mail"  # へッダ部分だけ抽出
  done | nkf -m | sed -n 's/^From: //p' |sort |uniq -c |sort -nr

mailrank コマンドを使って投稿者ランキングを調べるにはコマン ドラインから次のように実行する。

% mailrank ~/Mail/ml/foo
     27 Satoru Takabayashi <satoru@...>
     18 Toshiyuki Masui <masui@...>
      8 Koji Tsukada <tsuka@...>

この例では ~/Mail/ml/foo ディレクトリに、1メール = 1ファイル で保存されているメールを対象に投稿者ランキングを集計している。

xface-gallery

xface-gallery を紹介する前に、X-Face について説明する。 X-Face は、メールのへッダに付加できる 48×48 ピクセルの白黒 2値のビットマップ画像である。メールに付加されたX-Face 画像は X-Face 対応のメーラーで読むことができる。私は、3年ほど前に X-Face の存在を知り、以来、自筆の「なまず君」のイラストを X-Face としてメールにつけている。

X-Faceのついたメールの例
X-Faceのついたメールの例

上のなまず君の X-Face は次のような文字列としてメールのへッダ に付加されている。

X-Face: ']LG0eU\ZOAZuj!aa4#GU(:U#7sS-@y6b(({~0z8.UUiu\F#dE\8(8@4sVnc68WE>OxquYD}Q"jfR8{@&6@dgW&!DGQ>!YOOd@JZ0>Ik}=%IY`Pc=jv+Y0ZL(DQ)r/J*i

X-Face を作るには、48×48 ピクセルの白黒 2値のビットマッ プ画像を用意し、Web上の Online X-Face Converter *9 というサービスを使って 変換するとよい。compface というツールと Emacs 用の X-Face ユーティリティ *10 を組合せて作成する方法もあるが、 Web上のサービスを利用した方が楽である。

Online X-Face Converter にファイルをアップロードすると
Online X-Face Converter にファイルをアップロードすると
X-Face が生成される
X-Face が生成される

自分の出すメールに X-Face をつけるには、このページで生成され た X-Face: ... という文字列をメールのへッダに付加すればよい。 X-Face 対応のメーラーに関する情報は津邑公暁氏によるページ *11 に詳しい。

前置きが長くなったが、xface-gallery は、指定したディレクトリ 以下のすべてのメールから X-Face を抽出して、一枚の大きなタイ ル状の画像ファイルを作るためのツールである。ある日、手元にど のくらい X-Face が集まっているかを知りたくなって作成した。 ~/Mail 以下のメールを対象に xface-gallery を実行したところ、 191個の X-Face が見つかり、次のような画像が生成された。

% xface-gallery ~/Mail
191 x-faces found.

xface-galleryが生成した画像
xface-galleryが生成した画像

xface-gallery のソースコード

  #! /bin/zsh
  test -z "$1" && echo "Usage: xface-gallery <directory>" && exit
  i=0
  tmpdir=xface-gallery.$$
  mkdir $tmpdir
  find "$1" -type f |xargs xface-extract |sort |uniq |\
  while read -r line; do
      echo -E "$line" | xface2png > $tmpdir/`printf "%04d" $i`.png;
      i=`expr $i + 1`
  done 
  echo "$i x-faces found."
  
  x=`perl -e "print int(sqrt($i))"`
  rm -f xface-gallery.png
  montage -geometry 48x48 -tile $x $tmpdir/*.png  xface-gallery.png
  rm -rf $tmpdir

xface-gallery は、zsh*12 を用いたシェ ルスクリプトとして作成した。次のような流れで処理を行っている。

  1. 指定されたディレクトリ以下のメールから find と xface-extract を用いて X-Face を抽出し
  2. それぞれの X-Face を xface2png を用いて PNG画像に変換し
  3. 最後に ImageMagick の montage コマンドで一枚の大きなタイル状の画像ファイル (xface-gallery.png) を生成する

xface-gallery の中で使われている xface-extract と xface2png は私が作成したシェルスクリプトである。xface-extract はメール のへッダから X-Face の抽出を行う。コマンドライン引数に複数の メールのファイルが指定されたときは、1 行につき1つの X-Face を連続的に出力する。sed と perl を組合せた複雑な処理を行って いるのは、複数行にまたがってメールのヘッダに含まれている X-Face を正しく抽出するためである。perl -n00le の 00 の部分 はパラグラフモードで入力を読み込む (空行単位で読み込む) ため のおまじないである。

xface-extract のソースコード

  #! /bin/sh
  sed -n -e '/^X-Face:/,/^[^ \t]/ p' "$@" \
      | perl -pe 's/^X-Face:\s*/\n /' \
      | perl -ne 'print if /^$/ or /^[ \t]/' \
      | perl -n00le 's/\s//g; print'

xface2png は X-Face を PNG画像に変換する処理を行う。標準入力 から読み込んだ X-Face を uncompface で復号し、その頭に "/* Width=48, Height=48 */" という情報を付加したものを netpbm *13 の icontopbm と ImageMagick の convert コマンドを用いて PNG 画像に変換してい る。なぜこのような処理が必要なのか実はよくわかっていないが、 Web 上で X-Face の復号方法を調べているうちに見つけたテクニッ クをそのまま流用している。uncompface は compface*14 のパッ ケージに含まれているツールである。

xface2png のソースコード

  #!/bin/sh
  { echo '/* Width=48, Height=48 */'; uncompface; } | icontopbm | convert - png:-

X-Face は Emacs のマニアを中心として広まったという経緯があり、 一般にはまだあまり普及していないようである。メールをちょっと 楽しくする小道具として、X-Face をぜひ試してみてほしい。

メール関連のツールまとめ

メールを処理する小粒なツールとして mailrank と xface-gallery を紹介した。メールは基本的にへッダと本文が空行で分離されただ けのシンプルなフォーマットであるため、Unix 流のテキスト処理 との相性がいい。ここでは扱わなかったが、マルチパート形式の添 付ファイルを扱うには、シェルスクリプトから metamail コマンド *15 を使ったり、Ruby で TMail ライブラリ *16 を使うと 楽である。ちょっとした工夫で便利なツールを作れるでみなさんも 試してみてほしい。

画像関連のツール

長い間、私は Unix はテキスト処理には適しているが、画像処理に 関してはそうでもない、と考えていた。しかし、あるとき ImageMagick を知ってからこの考えは変わった。ImageMagick はコ マンドラインから画像処理を行うためのツールである。たとえば、 室内でデジタルカメラで撮影した写真の色合いが全体的に暗い場合、 コマンドラインから

% mogrify -gamma 1.4 *.jpg

のように実行すれば、カレントディレクトリのすべての JPEG ファ イルに対してガンマ補正*17をかけて色合いを明るくすることができ る。

ガンマ補正 1.4 をかける前とかけた後の画像

ガンマ補正をかける前の画像
ガンマ補正をかける前の画像
ガンマ補正をかけた後の画像
ガンマ補正をかけた後の画像

色合いの微妙な調整は GIMP などの画像編集ソフトのトーンカーブ の機能を使わないと難しいが、mogrify を使った大ざっぱな調整で も役に立つことは多い。ここでは画像ファイルを扱うために作った 小粒なツールを紹介する。

thumbnail

Web ページを作っているときに、画像のサムネイル (縮小画像) が ほしくなるときがある。大きすぎる画像をそのまま Webページに貼 ると不格好であるため、小さい画像をクリックすると元の大きな画 像が現われる、という仕組みにしたいからだ。サムネイルを作成す るには GIMP などの画像編集ソフトを使う方法もあるが、 ImageMagick の convert コマンドを使った方が楽である。たとえ ば foo.png を 50% のサイズに縮小した画像 foo-mini.png を作る にはコマンドラインから次のように実行すればいい。

% convert -geometry '50%' foo.png foo-mini.png

このように、convert コマンドは大変重宝するが、たくさんの画像 ファイルを扱う場合に、ファイルひとつづつに対してコマンドを打 つのは面倒である。そこで、複数の画像ファイルのサムネイルをま とめて作成するためのツール thumbnail を作成した。

thumbnail のソースコード

  #! /bin/zsh
  geometry="50%"
  
  # コマンドラインオションを処理
  while [ -n "$1" ]; do
      case $1 in
          # 横幅のサイズを指定するオプション
          -geometry )
              shift
              test -z "$1" && echo "-geometry needs an argument" && exit
              geometry=$1
              shift
              ;;
          * )
              break
              ;;
      esac
  done
  test -z "$1" && echo "Usage: thumbnail <file>..." && exit
  
  for filename in "$@"; do
      test -f "$filename" || continue # ファイルが存在しなければスキップ
      suffix="${filename##*.}"  # suffixの部分だけ取り出す (例: jpg)
      name="${filename%.*}"     # suffixより前の部分だけ取り出す
  
      # サムネイルのサムネイルを作らないように除外する
      echo "$filename" | grep -- "-mini.$suffix" >/dev/null && continue
  
      thumbname="$name-mini.$suffix"
      convert -geometry $geometry "$filename" "$thumbname"
      origsize=`identify "$filename" | awk '{print $3}'`   # 元のサイズ
      thumbsize=`identify "$thumbname" | awk '{print $3}'` # サムネイルのサイズ
      echo "$filename ($origsize) -> $thumbname ($thumbsize)"
  done

カレントディレクトリ以下のすべての PNG 画像に対して 50% のサ イズのサムネイルを作成するにはコマンドラインから次のように実 行すればいい。

% thumbnail -geometry '50%' *.png
lazy-web.png (648x646) -> lazy-web-mini.png (324x323)
xface-web-1.png (646x841) -> xface-web-1-mini.png (323x421)
xface-web-2.png (637x469) -> xface-web-2-mini.png (319x235)
xface.png (356x209) -> xface-mini.png (178x105)
xface-gallery.png (624x746) -> xface-gallery-mini.png (312x373)

thumbnail も xface-gallery と同様に zsh を用いたシェルスクリ プトとして作成した。Unix では伝統的にシェルスクリプトは /bin/sh を用いる場合が多いが、私はちょっと複雑なシェルスクリ プトを書く場合には /bin/zsh を使うようにしている。

これは zsh の拡張機能を使いたいためということもあるが、 「bashism」なシェルスクリプトを書くのを避けるためという意味 も大きい。bashism なシェルスクリプトとは、#! /bin/sh で始まっ ているにも関わらず、 bash*18 の拡 張機能を使っているもののことである。

実際、私が使っている Red Hat Linux 7.3 では /bin/sh がbash であるため、/bin/sh のつもりでシェルスクリプト書いていても、 知らず知らずに bash の拡張機能を使ってしまい、他のプラット フォームの /bin/sh で動かないものができあがってしまうことが 多い。何が bash 拡張なのか調べるのは面倒であるし、自分が使う Unix ホストには必ず zsh が入っているので、迷ったときは zsh のシェルスクリプトとして書くようにしている。

dropshadow

Webページに画像を貼るときに、縁取りの線や影をつけたくなると きがある。たとえば下の画像は、端末のスクリーンショットをとっ たものだが、縁取りの線すらないので、そのまま白地のWebページ に貼りつけると、背景と一体化してしまう。

縁取りの線すらない画像
縁取りの線すらない画像

GIMPなどの画像編集ソフトを使えば縁取りの線や影をつけることは できるが、横着な私としては、やはりコマンド一発で行いたい。そ こで、画像に縁取りの線や影をつけるためのツールdropshadow を 作成した。

先ほどの端末のスクリーンショットに対して縁取りの線と影をつけるに はコマンドラインから次のように実行する。

% dropshadow -b terminal-1.png > terminal-2.png

縁取りの線と影をつけた後の画像
縁取りの線と影をつけた後の画像

また、-f オプションを指定すると写真のような枠をつけることも できる。

% dropshadow -f imoimo-1.jpg > imoimo-2.jpg

枠と影をつける前の画像とつけた後の画像

枠と影をつける前の画像
枠と影をつける前の画像
枠と影をつけた後の画像
枠と影をつけた後の画像

dropshadow のソースコード

  #! /usr/bin/env perl
  use strict;
  use Image::Magick;
  use Getopt::Long;
  
  my $shadow_width = 10;
  my $with_border = undef;
  my $with_frame  = undef;
  my $without_shadow = undef;
  my $border_margin = 0;
  my $border_color = "black";
  my $shadow_color = "gray75";
  
  main();
  
  sub main {
      parse_options();
      my $image = read_image();
      draw($image);
      write_image($image);
  }
  
  # Getopt::Long モジュールを用いてコマンドラインオプションを解析する
  sub parse_options {
      Getopt::Long::Configure('bundling');
      GetOptions('w|width=i'  => \$shadow_width,
                 'b|border'   => \$with_border,
                 'f|frame'    => \$with_frame,
                 'n|no-shadow' => \$without_shadow,
                 'c|shadow-color=s'    => \$shadow_color,
                 'C|border-color=s'    => \$border_color);
  }
  
  sub read_image {
      my $image = new Image::Magick;
      if (@ARGV >= 1) {
          $image->ReadImage($ARGV[0]); # ファイルから画像を読み込む
      } else {
          $image->ReadImage('-');      # 標準入力から画像を読み込む
      }
      return $image;
  }
  
  sub draw {
      my ($image) = @_;
      draw_frame($image)  if $with_frame;
      draw_border($image) if $with_border || $with_frame;
      draw_shadow($image) unless $without_shadow;
      process_transparent_color($image);
  }
  
  sub draw_frame {
      my ($image) = @_;
      $image->Set(bordercolor => 'gray');
      $image->Border(geometry => "1x1");
      $image->Set(bordercolor => 'white');
      my $framewidth = $shadow_width / 2;
      $image->Border(geometry => "${framewidth}x${framewidth}");
  }
  
  sub draw_border {
      my ($image) = @_;
      $image->Set(bordercolor => $border_color);
      $image->Border(geometry => "1x1");
  }
  
  sub draw_shadow {
      my ($image) = @_;
      my ($width, $height, $magick) =  $image->Get('width', 'height', 'magick');
      if ($magick eq "GIF" || $magick eq "PNG") {
          # 透過色を設定する。 #facade は使われていないことが前提...
          $image->Set(bordercolor => '#facade');  
      } else {
          $image->Set(bordercolor => 'white');
      }
  
      $image->Border(geometry => "${shadow_width}x${shadow_width}");
      $image->Crop(x => $shadow_width, y => $shadow_width);
      my $width2  = $width + $shadow_width;
      my $height2 = $height + $shadow_width;
  
      $image->Draw(fill => $shadow_color, primitive=> 'rectangle', 
                   stroke => $shadow_color,
                   points => "$width,$shadow_width $width2,$height2");
      $image->Draw(fill => $shadow_color, primitive=> 'rectangle', 
                   stroke => $shadow_color,
                   points => "$shadow_width,$height $width2,$height2");
  }
  
  sub process_transparent_color {
      my ($image) = @_;
      my ($magick) =  $image->Get('magick');
  
      if ($magick eq "GIF" || $magick eq "PNG") {
          $image->Transparent(color => '#facade'); # 透過色を設定
      }
  }
  
  sub write_image {
      my ($image) = @_;
      my ($magick) =  $image->Get('magick');
      binmode STDOUT;
      $image->Write("$magick:-");
  }
  

dropshadow は ImageMagick に付属する PerlMagick という Perl 用のライブラリを用いて作成した。PerlMagick はバージョンによ る非互換があるらしく、この dropshadow コマンドを別の計算機に 持っていったら動かないことがあった。最新の ImageMagick 5.5.1 では動作することを確認している。

sync-day-by-day

デジタルカメラを手に入れてから写真をよく撮るようになった。フィ ルムのカメラではほとんど写真を撮らなかったが、デジタルカメラ ならゴミのような写真を撮りまくってもコストを気にする必要がな く、気楽に撮れる。そんなふうにして、毎日のように写真を撮って いると、写真の整理をどうするか考えなければいけなくなった。1 枚1 枚の写真にファイル名をつけていたら気が遠くなってしまうし、 残しておきたい写真を厳選するのも面倒である。そこで、 2002/11/15 のような日付ごとのディレクトリに写真を分類すると いう方針を採ることに決めた。

私が使っているデジタルカメラは写真に dcp_0001.jpg, dcp_0002.jpg のようなファイル名をつけて、コンパクトフラッシュ カードに保存する。ファイルのタイムスタンプには写真を撮った日 時が記録されているので PC へファイルをコピーするときはこのタ イムスタンプの情報を元にすればよい。そこで、タイムスタンプを 元にして日付ごとのディレクトリに分類しながらファイルのコピー を行うツール sync-day-by-day を作成した。sync-day-by-day は 次のような特徴を持っている。

/mnt/camera にマウントしたコンパクトフラッシュから pictures というディレクトリに、ファイルをコピーするにはコマンドライン から次のように実行する。

% sync-day-by-day /mnt/camera pictures
/mnt/camera/dcp_0001.jpg -> pictures/2002/11/13/dcp_0001.jpg
/mnt/camera/dcp_0002.jpg -> pictures/2002/11/13/dcp_0002.jpg
/mnt/camera/dcp_0003.jpg -> pictures/2002/11/13/dcp_0003.jpg
/mnt/camera/dcp_0004.jpg -> pictures/2002/11/13/dcp_0004.jpg
/mnt/camera/dcp_0005.jpg -> pictures/2002/11/13/dcp_0005.jpg
/mnt/camera/dcp_0006.jpg -> pictures/2002/11/14/dcp_0006.jpg
/mnt/camera/dcp_0007.jpg -> pictures/2002/11/14/dcp_0007.jpg
/mnt/camera/dcp_0008.jpg -> pictures/2002/11/15/dcp_0008.jpg
/mnt/camera/dcp_0009.jpg -> pictures/2002/11/15/dcp_0009.jpg
/mnt/camera/dcp_0010.jpg -> pictures/2002/11/15/dcp_0010.jpg
/mnt/camera/dcp_0011.jpg -> pictures/2002/11/15/dcp_0011.jpg

sync-day-by-day のソースコード

  #! /usr/bin/env ruby
  require 'date'
  require 'ftools'
  
  def usage
    puts "Usage: sync-day-by-day <source directory> <destination directory>"
    exit
  end
  
  def nodir(dir)
    puts "No such directory: " + dir
    exit 1
  end
  
  def same_file? (f1, f2)
    File.symlink?(f1) == false && File.symlink?(f2) == false &&
      File.file?(f1) && File.file?(f2) &&
      File.size(f1) == File.size(f2) && File.mtime(f1) == File.mtime(f2)
  end
  
  def parse_options
    usage if ARGV.length != 2
    nodir ARGV[0] unless File.directory?(ARGV[0])
    nodir ARGV[1] unless File.directory?(ARGV[1])
    return ARGV
  end
  
  def datedir(date)
    sprintf("%02d/%02d/%02d", date.year, date.month, date.day)
  end
  
  def copy(src, dest)
    atime = File.atime(src)
    mtime = File.mtime(src)
    File.copy src, dest
    File.utime(atime, mtime, dest)
  end
  
  def sync_day_by_day(src, dest)
    Dir.glob("#{src}/*") {|src_filename|
      next unless File.file?(src_filename)
      dest_dir = File.join(dest, datedir(File.mtime(src_filename)))
      dest_filename = File.join(dest_dir, File.basename(src_filename))
      unless File.file?(dest_filename) && same_file?(src_filename, dest_filename)
        printf("%s -> %s\n", src_filename, dest_filename)
        File.mkpath(dest_dir) unless File.directory?(dest_dir)
        copy(src_filename, dest_filename)
      end
    }
  end
  
  def main
    src, dest = parse_options
    sync_day_by_day(src, dest)
  end
  
  main

sync-day-by-day は Ruby に標準で付属している ftools.rb とい うライブラリを使ってファイルのコピー (File.copy) や 2002/11/13 のような多階層のディレクトリの作成 (File.mkpath) を行っている。

画像関連のツールのまとめ

画像を処理する小粒なツールとして thumbnail, dropshadow, sync-day-by-day を紹介した。Web ページやレポートなどで大量の 画像ファイルを扱う場合に、こうしたツールを用意しておくと作業 がずいぶん楽になる。画像編集ソフトでひとつづつ画像ファイルを 開いて処理するのが面倒だと感じたことがある方はぜひ試してほし い。

おわりに

今回は、私が開発した小粒なツールをWeb関連、メール関連、画像 関連に絞ってとりあげ、あわせて小粒なツールのプログラミングの ノウハウを紹介した。こうした小粒なツールを作ることは日常の作 業を快適にするだけでなく、プログラミングに上達するいい方法だ と思っている。おそらく皆さんの ~/bin ディレクトリにも、小粒 なツールがそろっているのではないかと思う。便利なツールやノウ ハウなどがあったらぜひ教えてもらえるとうれしい。

なお、本連載で作成したプログラムのソースコードは筆者のページ *19 から入手可能である。

おまけ: 超小粒なツール

あるとき、PC のスピーカが正常に動作しているかを調べるために、 音を鳴らす実験をしたくなった。しかし、音を鳴らす方法がとっさ に思いつかずに、一瞬考えたのちに端末でビープ音を鳴らせばいい と気づいた。そのときに作ったツールが beep である。

beep のソースコード

  #! /usr/bin/env perl
  print "\x07";

beep は \x07 という制御記号を使って端末からビープ音を鳴らす という、たった 2行の超小粒なツールである。コマンドラインから echo ^G *20 と実行してビープ音を鳴 らすこともできるが、コマンドを作っておいた方がわかりやすい。 コマンドラインから次のよう実行するとピッと音が鳴る。

% beep

ただこれだけのツールだが sleep コマンドと組合せると

% sleep 180 && beep  # 3分後にピッと鳴る

といった応用や、zsh を使っているなら

% repeat 100 beep    # 100回ビープ音を鳴らす

といったこともできる。今の速い計算機で repeat 100 beep を実 行したときと、昔の遅い遠い計算機で実行したときではビープ音の 間隔が変わって違う音色に聞こえる。perl のプロセス呼び出しに かかる時間が影響するようである。こんな些細なツールでもなかな か遊べるところがおもしろい。


Satoru Takabayashi

*1Martin Fowler 著, 児玉 公信, 友野 晶夫, 平澤 章, 梅澤 真 史訳『リファクタリング』 (ピアソン・エデュケーション) 2000
*2<http://0xcc.net/unimag/>
*3「GIF とLZW圧縮伸長法の特許について」というWebサイトに詳しくまとま められている。 <http://www.geocities.co.jp/SiliconValley/3453/gif_info/lzw_patent_jp.html>
*4<http://gimp.org/>
*5~/public_html は自分のホームディレクトリの下 の public_html ファイルである。私の場合は /home/satoru/public_html。
*6<http://www.imagemagick.org/>
*7コマンドラ イン1行で行う仕事のこと。さまざまなコマンドを組み合わせるこ とが多い。
*8ネットワーク漢字フィルター<http://www.ie.u-ryukyu.ac.jp/~kono/nkf/>
*9<http://www.dairiki.org/xface/>
*10<http://www.jpl.org/elips/>
*11<http://roguelife.org/x-face/>
*12<http://www.zsh.org/>
*13<http://netpbm.sourceforge.net/>
*14<http://freshmeat.net/projects/compface/>
*15<http://bmrc.berkeley.edu/~trey/emacs/metamail.html>
*16<http://www.loveruby.net/ja/prog/tmail.html>
*17ディスプレイの特性に合わせて画像の 明るさを調整すること
*18<http://www.gnu.org/software/bash/bash.html>
*19<http://0xcc.net/unimag/>
*20zsh や bash, tcsh で^G という制御コードを入力する には Ctrl+v Ctrl+g を連続して打つ