JAMADAM.COM

RSS Subscribe to my RSS feed

Stripe Calendar

Sep, 2010
Aug 1213141516171819202122232425262728293031 Sep 123456

Entry: Perlで差分バックアップスクリプト

Perlで差分バックアップスクリプト

Initial post: 2009.06.24 | Last modified: 2009.08.15

[2009-08-15] バキューム機能つけた

2006年にpdumpfsっぽいPerlスクリプトを書いた。glastreeの存在は知っていたけど、敢えて自分で作ってみた。それ以来、仕事でもプライベートでも重宝していたんだけど、最近問題発生。職場のファイルサーバーには百万越えのファイル数があって、毎日数万の差分が発生し、バックアップに5時間かかっていた。こいつはまずいということで手直しした。結果、取りあえず、自宅では以前の10倍高速になった。

ちなみに、今回はglastreeのソースを追って参考にしてみたらほぼ同じ内容になってしまったんだけど、glasstreeとの違いは下記のとおり。

  • バックアップの管理が日付に依存しないので任意の頻度(例えば2時間おきとか)で実行できる
  • コピーされたログを残す
  • コアモジュール以外に依存しない
  • 差分がない場合はバックアップを自動削除

ログが残るってのはなかなか便利です。差分リスト=作業履歴なので。

使い方

perl ddump.pl [バックアップ対象] [保存場所]

バックアップポイントの数の上限を指定。あぶれた分は古い方から削除される。

perl ddump.pl --vacuum=400 [バックアップ対象] [保存場所]

ソース

use strict;
use warnings;
use utf8;
use English;
use Getopt::Long;
use File::stat;
use File::Copy;
use File::Path;

binmode(STDIN,  ":utf8");
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
use open IO  => ":utf8";

use vars qw(%QUERY %stat_idx); # 設定
use vars qw($log $copy_count $file_count $symlink_count); # ログ関係変数
use vars qw($tgt_path $newdir $olddir); # ディレクトリ

main();

sub main {
    %stat_idx = ('mode' => 2, 'size' => 7, 'utime' => 9);

    %QUERY = ('mode' => 1, 'size' => 1, 'utime' => 1);
    GetOptions(\%QUERY, 'mode=s', 'size=s', 'utime=s', 'vacuum=i');

    ### 引数の整備と検証
    $ARGV[0] = &fixPath($ARGV[0]);
    $ARGV[1] = &fixPath($ARGV[1]);
    -d $ARGV[0] or die "$ARGV[0] not found";
    -d $ARGV[1] or die "$ARGV[1] not found";
    ### 引数を分離
    ($tgt_path, my $tgt_dir) = ($ARGV[0] =~ m!(.*)?/([^/]+)$!);

    if (opendir(DIR, "$ARGV[1]")) {
        my @dirs = sort grep{ -d "$ARGV[1]/$_" && /^\d+$/} readdir( DIR );
        close(DIR);
        ### 古いディレクトリを削除
        if ($QUERY{vacuum}) {
            for (my $i = 0; $i < $#dirs - $QUERY{vacuum}; $i++) {
                #print $dirs[$i] . " ";
                rmtree("$ARGV[1]/$dirs[$i]");
            }
        }

        ### 前回バックアップの時刻ディレクトリ名を取得
        $olddir = "$ARGV[1]/". (pop(@dirs) or '');
    }

    ### 時刻ディレクトリの作成
    my ($sec, $min, $hour, $mday, $mon, $year) = localtime($BASETIME);
    my $new_time =
        sprintf('%04d%02d%02d%02d%02d%02d', $year+1900, $mon+1, $mday, $hour, $min, $sec);
    $newdir = "$ARGV[1]/$new_time~";
    umask 0;
    mkdir($newdir, 0755) or die "cannnot make $newdir";

    ### ログファイル生成
    open($log, ">>$newdir/ddumplog.txt") or die "cannnot make log file";
    print $log <<EOF;
-----------------------------------------------------------------
-- ddump -- $new_time
-----------------------------------------------------------------
EOF

    ### ログ記録用変数
    $copy_count = 0;
    $file_count = 0;
    $symlink_count = 0;

    ### バックアップ
    &backup($tgt_dir);

    ### ログファイルに結果出力
    print $log "$copy_count/$symlink_count/$file_count (Copyed/symlink/Total)\n";
    print $log (time() - $BASETIME). "sec passed\n";
    close($log);

    if (! $copy_count) {
        ### コピーなしならディレクトリ削除
        rmtree($newdir);
    } else {
        ### ファイル名確定
        rename($newdir, substr($newdir, 0 ,length($newdir) - 1));
    }
}

### --------------------------------------------
### 再帰的ディレクトリバックアップ
### --------------------------------------------
sub backup {

    my $cwd = shift;
    my $tgt_cwd = "$tgt_path/$cwd";
    my $old_cwd = "$olddir/$cwd";
    my $new_cwd = "$newdir/$cwd";

    ### ディレクトリの作成
    &copydirstat($tgt_cwd, $new_cwd);

    ### カレントディレクトリ内の全ファイルを配列に格納
    opendir(DIR, $tgt_cwd) or return 0;
    my @files_and_dirs = readdir(DIR);
    close(DIR);

    my @files = grep { -f "$tgt_cwd/$_" or -l "$tgt_cwd/$_" } @files_and_dirs;

    ### @files_and_dirsのうち、ファイルを処理
    foreach my $a_file (@files) {
        my $old_file = "$old_cwd/$a_file";
        my $new_file = "$new_cwd/$a_file";
        my $tgt_file = "$tgt_cwd/$a_file";
        my $command = 1; # 0: nothing, 1: hard link 2: copy

        if (-l $tgt_file) {
            symlink(readlink($tgt_file), $new_file);
            $symlink_count++;
            $command = 0;
        }

        elsif (-f $old_file) {
            my $old_stat    = stat($old_file);
            my $crnt_stat   = stat($tgt_file);

            ### 更新ありならコピー
            foreach my $key (keys %stat_idx) {
                if ($QUERY{$key} and
                    $old_stat->[$stat_idx{$key}] ne $crnt_stat->[$stat_idx{$key}]) {

                    $command = 2;
                    last;
                }
            }
        } else { ### ファイルがなければコピー
            $command = 2;
        }

        ### コピー
        if ($command == 2) {
            copystat($tgt_file, $new_file);

            $copy_count++;

            ### ログ出力
            utf8::decode($new_file);
            print $log "$new_file\n";
        }

        ### ハードリンク
        elsif ($command == 1) {
            link($old_file, $new_file);
        }

        $file_count++;
    }

    ### @files_and_dirsのうち、ディレクトリを再帰処理
    my @dirs = grep { -d "$tgt_cwd/$_"
                    and not -l "$tgt_cwd/$_"
                    and $_ ne '.'
                    and $_ ne '..' } @files_and_dirs;

    foreach my $a_dir (@dirs) {
        &backup("$cwd/$a_dir") or print "Error at $cwd/$a_dir\n";
    }

    return 1;
}

### --------------------------------------------
### 属性丸ごとファイルコピー
### --------------------------------------------
sub copystat {
    my ($from, $to) = @_;
    my $stat = stat $from;

    copy($from, $to) or die "cannot copy $from to $to";
    chown($stat->uid, $stat->gid, $to) if $EUID == 0;
    chmod($stat->mode, $to);
    utime($stat->mtime, $stat->mtime, $to);
}

### --------------------------------------------
### 属性真似てディレクトリ作成
### --------------------------------------------
sub copydirstat {

    my ($from, $to) = @_;
    my $stat = stat $from;

    mkdir $to, 0555 or die "cannot mkdir $to";
    chown ($stat->uid, $stat->gid, $to) if $EUID == 0;
    chmod ($stat->mode, $to);
    utime ($stat->mtime, $stat->mtime, $to);
}

### --------------------------------------------
### ファイルパスの整備
### --------------------------------------------
sub fixPath {
    my $in = $_[0];
    $in =~ s!^!\./! unless ($in =~ m!^\.*/!);
    $in =~ s!/$!!;
    return $in;
}

 

Entry: はてブ窓にコメント表示した

はてブ窓にコメント表示した

Initial post: 2009.07.29 | Last modified: 2009.07.29

連日、日付越えの激務のため趣味の開発などには全く身が入らない、なんていいつつ、ささやかな成果物。はてブ窓にコメントを表示するようにした。これのためにどうでもいいコメントもつけた。

関係ないけど、はてブatomfeedのofパラメータってなんであんな回りくどい仕様なんだろう。このブログでは必要なデータ取得するのに下記みたいなことをやってみたけど、使い方間違ってる?

var hatebu_epp = 20; // API固定の取得件数
var disp_epp = 5; // 例えば5件表示
var page = 5; // 例えば5ページ目を取得
var ppr = hatebu_epp / disp_epp; // 1リクエストで4ページ分
var request_offset = Math.floor((page - 1) / ppr) * hatebu_epp; // ofパラメータは20
var local_offset = Math.floor((page - 1) % ppr) * disp_epp; // 取得したデータ中のオフセットは0

 

Entry: 非root権限でCPANモジュールをインストールする(焼増し)

非root権限でCPANモジュールをインストールする(焼増し)

Initial post: 2009.07.04 | Last modified: 2009.07.04

非root権限でCPANモジュールを使う方法を調べた。Debian lenny(最小限のセットアップ)にて。2006年頃までにすべての情報が出尽くしてるのに何を今さら。

cpanの初期設定をする。設定項目「makepl_arg」はモジュールのインストール先ディレクトリ。PREFIX=~/perlと記載することで~/perl下に配置される。設定ファイルは下記のように書き換える。

$ vi ~/.cpan/CPAN/MyConfig.pm

- 'makepl_arg' => q[],
+ 'makepl_arg' => q[PREFIX=~/perl],

依存関係のあるモジュールを芋づる式にインストールするとき、後に入るモジュールのテストスクリプトが、直前の依存モジュールをincludeできなくてつまづく。これを回避するため、ユーザーの環境変数PERL5LIBにパスを設定する。~/.bashrcに下記を追記すれば楽。どうやらものによって下記のような2つのパスにインストールされるらしい。

$ vi ~/.bashrc

export PERL5LIB=~/perl/lib/perl/5.10.0:~/perl/share/perl/5.10.0

最初の「~/perl」は先ほどMyConfigのmakepl_argに追加したパス、それ以降は環境に依存するようなので一度失敗してみてパスを確認して再試行。もっといい方法ないのか。そのためのlocal::libなのかどうなのか、またの機会に調べてみよう。

参考サイト
一般ユーザ環境におけるCPANモジュールの使い方
Using CPAN with a non-root account

Entry: ブックマーク

ブックマーク

Initial post: 2009.05.22 | Last modified: 2009.05.22

TwitterでニュースFeedをフォローし出してからおもしろ記事がありすぎて全然追えない。

Entry: ブックマーク

Entry: javascript - jquery.limitCharWidth.js

javascript - jquery.limitCharWidth.js

Initial post: 2009.01.17 | Last modified: 2009.02.01

image最近のコメント一覧のガジェットを、以前はサーバー側で長さ調整していたんですが、クライアント側で処理するように変更しました。これでユーザー環境に依存せずに最適なところで省略されるはず。ただし、文字サイズの変更とかガジェットの移動時に更新するとかの処理はまた今度。あと、例によって(どの例?)、IEでの動作検証は不十分です。

これのためにjqueryプラグインを作りました。改善の余地は大いにあり。

jquery.clipstr.js

使い方。widthは省略すると親のwidthに合わせてくれるかもしれない。

$('.boxsize_limited').limitCharWidth({
    width       : 220,
    instead_str : '...',
    set_title   : false
});

これ作ってて、関係ないところでハマった。上記の第二引数の後にカンマ入れてもFirefoxやSafariでは動くけどIE6,7で動かないらしい。なんでそんなところにカンマ入れるかと言えばメンテナンス性が向上するためで、Perlでは推奨されてたはず。

[2009.1.18追記]

少し直した。この機能のためにspanタグを自分で用意する必要があったのを改善。

[2009.1.19追記]

ちょっとバグってたので直した。

widthって指定させる必要があまりない気がしてきた。

[2009.1.23追記]

リサイズの検出の仕方がわからんなあ。とりあえず、元に戻せるようにunclipstrメソッドを追加。

$('.boxsize_limited').unlimitCharWidth();

[2009.1.28追記]

名前変えた。jquery.limitCharWidth.js。勇気を出してjQuery公式サイトにも登録してみようかな。

...つーかIE NetRendererで動いてねーし。ここ、たぶんスクリプトの読み込みに時間かかると無視されるから、実際の動作と必ずしも一致しないんだよなあ。やっぱマックのみではウェブ制作は無理だ。

...直ったっぽい。原因が2つあった。ひとつは、CSSのキーにはcss-prop(例えばfont-weight)とcssProp(例えばfontWeight)という2つの書式があって、jQueryのCSSメソッドでIEを相手にする場合はcssPropでないといけない(っぽい)。もうひとつは、IEのCSSでは多くのプロパティに対してinheritを指定することができなく、CSSメソッドでエラーが起きる(っぽい)。そんな訳で、inheritの代わりに、親の値を再指定することでinheritっぽいことをしてみた。けど、当初の目的を達成しているのか甚だ疑問。

[2009.2.1]

IE6でwidthを指定しない場合に動作していなかったのを改善。

ToDo

  • 対象文字列が子要素を持つとたぶんバグるので対策。
  • 内部で一時的に設置したspanが外のCSSの影響を受けないか検証。
  • 自分自身がリサイズされたことを知る方法があるのか。あるなら自動更新機能。
  • title属性使用の有無を引数で指定

 

Entry: Javascript - newtooltip version 0.1

Javascript - newtooltip version 0.1

Initial post: 2008.10.19 | Last modified: 2008.10.19

先日から勉強がてら改造していたjqueryプラグインがひと段落したのでアップしてみる。その名もjquery.newtooltip.js。半分くらい原型をとどめてないので、もともとの機能が維持できてるか謎。特に、背景画像のIE対応とか未確認。世の中にはtooltipのjqueryプラグインはたくさんあるので、すでにいいものがあるのかも知れないです。あくまで勉強。

動作イメージはこちら

下記のようにセットアップすると、title属性のあるタグは全部、マウスオーバーに反応するようになる。

// 一番簡単な使い方
$(document).ready(function(){
    
    $.tooltip_setup();
});

様々な初期設定。

// 様々な初期設定(下記はデフォルト)
$.tooltip_setup({
    in_json     : false,        // json書式を使用するか否か
    auto        : true,         // title属性から全自動で設定
    handler_on  : 'mouseover',  // tooltip表示用ハンドラ
    handler_off : 'mouseout',   // tooltip非表示用ハンドラ
    track       : true,         // mousemoveに追従する設定
    fade        : 200,          // フェード(msec)
    top         : 15,           // ポインタからの距離
    left        : 15,           // ポインタからの距離
    id          : "TOOLTIP"     // tooltip用id
    auto_css    : true,         // CSSをデフォルト値に設定
});

newtooltip.min.js
圧縮ファイル。

newtooltip.org.js
非圧縮ファイル。

Entry: Ubiquityで英辞郎検索

Ubiquityで英辞郎検索

Initial post: 2008.09.01 | Last modified: 2008.09.01

Firefoxのプラグイン「Ubiquity」のコマンドラインから英辞郎で検索。解説はここを参照。リンク先のITmedia検索用コマンドをちょっと書き換えただけだけど。

makeSearchCommand({
    name: "alc",
    url: "http://eow.alc.co.jp/{QUERY}/UTF-8/",
    icon: "/favicon.ico",
    description: "Searches 英辞郎 on the WEB for your words.",
});

普通にここからxpiをDLしてインストール。Control + スペースでコマンドラインインターフェースが開くのでcommand-editorと入力すると新規タブにフォームが現れる。そこにコマンドのソースコードを入力(コピペ)すると保存などせずとも即、コマンドが有効になる。

あとはコマンドラインに下記のように入力するだけでwordという単語を検索できる。

alc word

なんかUbiquity自体の説明をまだよく読んでないんで、使い方がまだよく分からないんですが、どうやら「selection」と入力すると選択領域が引数に渡るらしい。

alc selection

 ちょっと前から思ってたんですが、英辞郎に昨年あたりからついたスペル修正候補の提案機能はすごく便利なんだけど、googleの「もしかして」機能よりは劣ってる。この点、Ubiquity使ったら連携してもっと便利になりそうな気がしてきた。

[2008/09/02追記]

選択領域を引数として渡すのにselectionは必要なかったらしい。検索系コマンドの場合は引数なければ自動的に選択領域が渡る。そりゃそうだよな。selectionなんて打つのは冗長すぎ。

Entry: Perl - PerlMagickでテキスト処理

Perl - PerlMagickでテキスト処理

Initial post: 2008.05.27 | Last modified: 2008.05.27

heart_jamadam最近、プライベートでPerlMagickを覚えたので、長文いれたら勝手に折り返してくれるPerlスクリプトを作ってみた。こんなものを動的に作って何になるのかってところは不明です。

先日のPHP+GDとほぼ同じ出力が得られるのに加えて、ストロークとドロップシャドウっぽいことをやるという余計な機能もつけてみた。PHP+GDよりえらく重いけど、Perl+Magickのがライブラリがいろんな機能に対応していて楽しい。

ソースコードも出しちゃう。汚いですけど。

package NT::Image::Magick::AnnotateWithLineBreak;
use strict;
use warnings;
use Image::Magick;
use Encode qw(decode);
use encoding 'utf8';

###  ------------------------------------------------------------------------------
### Constractor
###  ------------------------------------------------------------------------------
sub new {
    
    my $class = shift;
    my $self = 
        bless {
            magick => Image::Magick->new(), 
            charset => 'utf8', 
            ngCharactorsForHead => 
                ',)]}、〕〉》」』】〟’”`≫。.・:;ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎ', 
            ngCharactorsForTail => 
                '([{〔〈《「『【〝‘“_≪', 
            ngCharactorsForSeparation => 
                '[a-zA-Z0-9¥'".,!¥?¥-]', 
            result => {width => 0, height => 0},
            @_
        }, $class;
    
    $self->Read($self->{file});
    
    return $self;
}

###  ------------------------------------------------------------------------------
### パラメータセットアップ
###  ------------------------------------------------------------------------------
sub setParam {
    
    my $self = shift;
    %$self = (%{$self}, @_);
}

###  ------------------------------------------------------------------------------
### メンバ変数magickにメソッドを委譲
###  ------------------------------------------------------------------------------
sub AUTOLOAD {
    
    no strict;
    my $self = shift;
    my $type = ref($self);
    my $name = $AUTOLOAD; $name =~ s/.*://;
    
    return $self->{magick}->$name(@_);
}

###  ------------------------------------------------------------------------------
### 文字入れ
### 戻り値:ハッシュリファレンス/要した幅と高さ
###  ------------------------------------------------------------------------------
sub breakLineAnnotate {
    
    my $self = shift;
    my %args = (
        x                       => 0,
        y                       => 0,
        width                   => undef,
        height                  => undef,
        pointsize               => 9,
        font                    => undef,
        text                    => '',
        fill                    => '#000000',
        'line-spacing'          => 4,
        'drop-shadow-depth'     => 0,
        'drop-shadow-color' => '#000000',
        'nice-stroke-width'     => 0,
        'nice-stroke-color'     => '#111111',
        @_);
    
    $args{width} = 
        (defined $args{width}) 
            ? $args{width} 
            : ($self->Get('width') - $args{x});
    
    $args{height} = 
        (defined $args{height}) 
            ? $args{height} 
            : ($self->Get('height') - $args{y});
    
    my $in_str = 
        utf8::is_utf8($args{text}) 
            ? $args{text} 
            : decode($self->{charset}, $args{text});
    
    $self->_putMultiLine(%args, text => $in_str);
    
    return $self->{result};
}

###  ------------------------------------------------------------------------------
### プリント
### 戻り値:整数/要した高さ
###  ------------------------------------------------------------------------------
sub _putMultiLine {
    
    my $self = shift;
    my %args = (@_);
    
    my $y_pos = $args{y};
    
    foreach my $line (split(/¥n/, $args{text})) {
        
        if (! $line) {
            
            $y_pos += $args{pointsize} + $args{'line-spacing'};
            next;
        }
        
        $y_pos = 
            $self->_putLogicalLine(
                %args, 
                text => $line, 
                y => $y_pos + $args{'line-spacing'}
            );
    }
    
    $self->{result}->{height} = $y_pos if ($y_pos > $self->{result}->{height});
    
    return $y_pos;
}

###  ------------------------------------------------------------------------------
### 論理行のプリント
### 戻り値:整数/要した高さ
###  ------------------------------------------------------------------------------
sub _putLogicalLine {
($__PACKAGE__::debugcount++ > 200) and $::gERROR->die(log => 'deep:'. $__PACKAGE__::debugcount);
    
    my $self = shift;
    my %args = (@_);
    
    $args{text} =~ s/^¥s//;
    
    my $pos1 = ($args{_last_length} or 1);
    my @box = 
        $self->QueryFontMetrics(%args, text => substr($args{text}, 0, $pos1));
    
    my $increment = ($box[4] > $args{width}) ? -1 : 1;
    
    ### Search for horizontal limit
    for (my $i = $pos1; $i > 0 and $i <= length($args{text}); $i += $increment) {
        
        @box = $self->QueryFontMetrics(%args, text => substr($args{text}, 0, $i));
        
        if ($increment == 1 and $box[4] > $args{width}) {
            
            last;
        }
        
        $pos1 = $i;
        
        if ($increment == -1 and $box[4] < $args{width}) {
            
            last;
        }
    }
    
    return $args{y} if (($args{y} + $box[5]) > $args{height});
    
    ### word wrapping
    if ($pos1 < length($args{text})) {
        
        while ($pos1 > 1) {
            
            my $next = substr($args{text}, $pos1, 1);
            
            if ($next and index($self->{ngCharactorsForHead}, $next) > -1) {
                
                $pos1--;
                next;
            } 
            
            my $last = substr($args{text}, $pos1 - 1, 1);
            
            if ($last and index($self->{ngCharactorsForTail}, $last) > -1) {
                
                $pos1--;
                next;
            }
            
            if ($last =~ /$self->{'ngCharactorsForSeparation'}/ and 
                $next =~ /$self->{'ngCharactorsForSeparation'}/) {
                
                $pos1--;
                next;
            }
            
            last;
        }
    }
    
    @box = $self->QueryFontMetrics(%args, text => substr($args{text}, 0, $pos1));
    $self->{result}->{width} = $box[4];
    $args{_last_length} = $pos1;
    
    my %args_common = (
        %args, 
        text    => substr($args{text}, 0, $pos1), 
        y       => ($args{y} + $box[5]), 
        );
    
    ### drop shadow
    for (my $i = 0; $i < $args{'drop-shadow-depth'}; $i++) {
        
        my %args_shadow = %args_common;
        
        if ($args{'nice-stroke-width'}) {
            
            $args_shadow{stroke} = $args{'drop-shadow-color'};
            $args_shadow{strokewidth} = $args{'nice-stroke-width'},
        }
        
        $self->Annotate(
            %args_shadow, 
            fill        => $args{'drop-shadow-color'},
            x           => $args_common{x} + 1, 
            y           => $args_common{y} + 1,
        );
    }
    
    ### stroke
    if ($args{'nice-stroke-width'}) {
        
        $self->Annotate(
            %args_common, 
            fill        => $args{'nice-stroke-color'},
            stroke      => $args{'nice-stroke-color'},
            strokewidth => $args{'nice-stroke-width'},
        );
    }
    
    ### Put text
    $self->Annotate(%args_common);
    
    ### Evaluate tail str
    if ($pos1 < length($args{text})) {
        
        return 
            $self->_putLogicalLine(
                %args, 
                y       => ($args{y} + $box[5] + $args{'line-spacing'}), 
                text    => substr($args{text}, $pos1)
            );
    }
    
    ### Returns bottom position of written box
    return $args{y} + $args{pointsize};
}

###  ------------------------------------------------------------------------------
### 結果取得
### 戻り値:interger/要した幅または高さ/
###  ------------------------------------------------------------------------------
sub getResult {
    
    return shift->{result}->{$_[0]};
}

return 1;

使い方

my $image = 
    NT::Image::Magick::AnnotateWithLineBreak->new(
        file => 'source/heart_jamadam.png', charset => 'euc-jp');

$image->breakLineAnnotate(
    text                => 
        '最近、仕事でGDを覚えたので、長文いれたら勝手に折り返してくれるPHPスクリプトを作ってみた。日本語禁則処理もできそうだなあ。', 
    font                => 
        './font/mika.ttf', 
    pointsize           => 10,
    fill                => '#000000',
    x                   => 10,
    y                   => 80,
    width               => 231,
    height              => 260,
    'nice-stroke-width' => 1, 
    'nice-stroke-color' => '#ffffff',
    'drop-shadow-depth' => 0, 
    'drop-shadow-color' => '#000000',
);

$image->breakLineAnnotate(
    text                => '今すぐクリック', 
    font                => 
        './font/ipa/ipagp0208_for_legacy_compatibility.ttf', 
    pointsize           => 14,
    fill                => '#000000',
    width               => 251,
    height              => 260,
    x                   => 155,
    y                   => 130,
    'nice-stroke-width' => 6, 
    'nice-stroke-color' => '#fafffa',
    'drop-shadow-depth' => 0, 
    'drop-shadow-color' => '#000000',
);

my ($format, $mime) = $image->Get('magick', 'mime');

print 'Content-type: '. $mime. "¥n¥n";
binmode(STDOUT);
$image->Write($format. ':-');

mikachan

[2008-5-27追記]
分割禁則というのにも、雰囲気だけ対応してみた。これで英文も概ね良好。

english script on photo

Entry: Perl - サムネイル自動生成をモジュールにしてみた

Perl - サムネイル自動生成をモジュールにしてみた

Initial post: 2008.05.16 | Last modified: 2008.05.16

以前作ったサムネイルの自動生成CGIをモジュール化してみた。もともと小さなプログラムだったので、途中何度も「やる意味あんのか?」っていう、コンセプトレベルで結構悩んだ。でも結果的には割と理にかなった構造になった気がする。そして、前回書いたAUTOLOADによる継承委譲もつかってみた。

package NT::Image::Magick::Thumbnail;
use strict;
use warnings;
use Image::Magick;

### -----------------------------------------
### コンストラクタ
### -----------------------------------------
sub new {
    
    my $class = shift;
    my $self = {
        magick => Image::Magick->new,
        file => undef,
        cache_dir => undef,
        max_width => 800,
        max_height => 800,
        @_};
    
    bless $self, $class;
    $self->Read($self->{file});
    
    return $self;
}

### -----------------------------------------
### 継承をメンバ変数magickに適用
### -----------------------------------------
sub AUTOLOAD {
    
    no strict;
    my $self = shift;
    my $type = ref($self);
    my $name = $AUTOLOAD; $name =~ s/.*://;
    
    return $self->{magick}->$name(@_);
}

### -----------------------------------------
### 画像リサイズ & キャッシュ出力
### -----------------------------------------
sub make {
    
    my $self = shift;
    my %args = (
        width => 0, 
        height => 0, 
        @_);
    
    if (my $cache_name = $self->cacheExists(%args)) {
        
        $self->Read($cache_name);
    }
    
    else {
        
        my ($width, $height) = $self->Get('width', 'height');
        
        ### 引数取得
        my $target_width = ($args{width} or 0);
        my $target_height = ($args{height} or 0);
        
        ### アスペクト比保持
        $target_height  ||= ($height * $target_width) / $width;
        $target_width   ||= ($width * $target_height) / $height;
        
        ### 上限判定
        $target_width = $self->{max_width} if ($target_width > $self->{max_width});
        $target_height = $self->{max_height} if ($target_height > $self->{max_height});
        
        ### 四捨五入と0回避
        $target_width = (int($target_width + 0.5) or 1);
        $target_height = (int($target_height + 0.5) or 1);
        
        ### リサイズ
        $self->Resize(width => $target_width, height => $target_height);
        
        ### キャッシュ出力
        if ($self->{cache_dir}) {
            
            my $cache_name = $self->getCacheName(%args);
            
            if (open(CACHE, "> ". $cache_name)) {
                
                binmode(CACHE);
                $self->Write(file => ¥*CACHE, filename => $cache_name);
                close(CACHE);
            }
        }
    }
}

### -----------------------------------------
### キャッシュ存在確認
### -----------------------------------------
sub cacheExists {
    
    my $self = shift;
    my %args = (
        width => 0, 
        height => 0, 
        @_);
    
    my $name = $self->getCacheName(%args);
    return (-e $name) ? $name : undef;
}

### -----------------------------------------
### キャッシュ名を得る
### -----------------------------------------
sub getCacheName {
    
    my $self = shift;
    my %args = (
        width => 0, 
        height => 0, 
        @_);
    
    if ($self->{cache_dir}) {
        
        my $new_query = '';
        $new_query = ($args{width}) ? 'x='. $args{width} : '';
        $new_query .= ($args{height}) ? '&y='. $args{height} : '';
        $new_query =~ s/^&//;
        
        my ($name, $ext) = ($self->{file} =~ /^(.+)¥.([^.]+)$/);
        return sprintf("%s/%s.%s.%s", $self->{cache_dir}, $name, $new_query, $ext);
    }
    
    return;
}

return 1;

使い方

#!/usr/bin/perl -w

### -----------------------------------------
### 初期設定
### -----------------------------------------
my $org_dir = '';
my $cache_dir = 'cache';
my $max_width = 800;
my $max_height = 800;
### -----------------------------------------

use strict;
use warnings;
use NT::Image::Magick::Thumbnail;
use CGI;

my $q = new CGI(); 

my %args = (
    width => scalar $q->param('x'), 
    height => scalar $q->param('y'));

### ソース
my $org_path = $org_dir. './'. $q->param('f');
-e $org_path or die 'file not found';

my $image = 
    NT::Image::Magick::Thumbnail->new(
        file => $org_path, 
        cache_dir => $cache_dir,
        max_width => $max_width, 
        max_height => $max_height
    );

if (! &binary_out($image->getCacheName(%args), $image->Get('mime'))) {
    
    $image->make(%args);
    
    my ($format, $mime) = $image->Get('magick', 'mime');
    
    ### ヘッダ出力
    print 'Content-type: '. $mime. "¥n¥n";
    
    binmode(STDOUT);
    $image->Write($format. ':-');
}

exit(0);

### -----------------------------------------
### 汎用バイナリ出力
### -----------------------------------------
sub binary_out {
    
    if (open(IN, $_[0])) {
        
        print "Content-Type: $_[1]¥n¥n";
        
        binmode(IN);
        binmode(STDOUT);
        
        read(IN, my $buf, -s $_[0]);
        print $buf;
        close(IN);
        
        return 1;
    }
    
    return 0;
}

以前書いたとおりのhtaccessを使えば、2回目以降CGIの起動がないので速いです。

ちなみに、このサンプルコードのために生まれて初めてCGI.pmを使ってみた。やっぱ、あるものは使うべきなのか。

[2008/05/16追記]

モジュールのほうでキャッシュ機能を活用してるので、実はこれだけでよかったりする。

#!/usr/bin/perl -w

### -----------------------------------------
### 初期設定
### -----------------------------------------
my $org_dir = '';
my $cache_dir = 'cache';
my $max_width = 800;
my $max_height = 800;
### -----------------------------------------

use strict;
use warnings;
use NT::Image::Magick::Thumbnail;
use CGI;

my $q = new CGI();

my $org_path = $org_dir. './'. $q->param('f');
-e $org_path or die 'file not found';

my $image = 
    NT::Image::Magick::Thumbnail->new(
        file => $org_path, 
        cache_dir => $cache_dir,
        max_width => $max_width, 
        max_height => $max_height
    );

$image->make(
    width => scalar $q->param('x'), 
    height => scalar $q->param('y'));

my ($format, $mime) = $image->Get('magick', 'mime');
print 'Content-type: '. $mime. "¥n¥n";
binmode(STDOUT);
$image->Write($format. ':-');

exit(0);