名古屋というところでゆるゆるとPerlを書いているissmと申しますこんにちは.先週イカ帽子を作りました.
さて,Webサイトなんかで,比較的小さなサイズの素材画像をひとまとめにして,用途によってその背景位置をずらすことでうまく表示する「CSS Sprite」という手法,もうご存知のことかと思います.次のエントリあたりが人気のようですね.
私もナビゲーション・メニュー画像(言い方古い?)をはじめ,マウスオーバーで変化するような画像なんかで,この手法を使っています.
しかしまぁ,この「CSS Sprite」,素材画像をまとめるための作業(デザイナさんから渡された「デザイン重視」なデータを「素材重視」に加工したり云々)や,各画像のサイズの把握,そして次のようなCSSの記述,中でも,画像のサイズから background-position
の計算,などなど,けっこうメンドクサイです.
#navi ul li { width: ...; height: ...; } #navi ul li a { display: block; width: 100%; height: 100%; background-image url(...); } #navi ul li#navi-item-1 a { background-position: ...; } #navi ul li#navi-item-2 a { background-position: ...; } #navi ul li#navi-item-3 a { background-position: ...; } #navi ul li#navi-item-1 a:hover { background-position: ...; } #navi ul li#navi-item-2 a:hover { background-position: ...; } #navi ul li#navi-item-3 a:hover { background-position: ...; }
また,次をはじめとする,CSS Spriteな画像やCSSを生成するWebサービスもありますが,これはこれで,つなげたいファイルを何度も選んだりするのがメンドクサかったりします.
ということで,本エントリでは,バラバラな1つ1つの素材画像をつなげて,background-position
の値なんかも自動で計算された状態でCSSとして出力する,あたりまでの処理をPerlでやってみます.というか,今日やってみたので,その記録的な感じです.
タイトルのとおりですが,次の2つのモジュールを使います.
画像の幅・高さを取得してくれます.
use Image::Size; my ($width, $height) = imgsize('/path/to/image.png');
画像をいろいろ加工してくれます.
use Image::Imlib2; my $img_base = Image::Imlib2->new(100, 100); my $img_load = Image::Imlib2->load('/path/to/image.png'); $img_base->blend($img_load, 1, 0, 0, 100, 100, 20, 20, 50, 50); # 画像を合成 $img_base->save('/path/to/image_saved.png'); # 保存
詳しくは,[/articles/advent-calendar/2009/casual/24.html:title=昨年のJperl Advent Calendarにaomushi510さんのナイスなチュートリアルがある]ので,こちらをご覧になるのがよいでしょう.
よくあるul
表記でのナビゲーションを考えます.
<div id="navi"> <ul> <li id="a1"><a href="#">acme</a></li> <li id="b1"><a href="#">casual</a></li> <li id="c1"><a href="#">english</a></li> <li id="d1"><a href="#">hacker</a></li> <li id="e1"><a href="#">meta_adcal</a></li> <li id="f1"><a href="#">perl6</a></li> <li id="g1"><a href="#">sym</a></li> <li id="h1"><a href="#">win32</a></li> </ul> </div>
対象となるディレクトリに,_manifest.pl
というファイルを置きます.
のような情報を定義します.
試行錯誤の結果,次のようにすると多少汎用的にできそうな予感です.
my $m = { # background-image プロパティを共有する部分の定義 base => { selector => '#navi ul li% a%', props => [ display => 'block', width => '160px', height => '50px', ], }, # URL上の画像パス(CSS記述時に利用) image_path => '.', # 画像連結なんかに関する定義 images => [ # 1段目 [ a1 => '', b1 => '', c1 => '', d1 => '', ], # 2段目 [ a2 => ['#a1', ':hover'], b2 => ['#b1', ':hover'], c2 => ['#c1', ':hover'], d2 => ['#d1', ':hover'], ], # 3段目 [ e1 => '', f1 => '', g1 => '', h1 => '', ], # 4段目 [ e2 => ['#e1', ':hover'], f2 => ['#f1', ':hover'], g2 => ['#g1', ':hover'], h2 => ['#h1', ':hover'], ], ], };
ようやく本題です.処理自体はたいしたことはしていません.
use strict; use warnings; use Image::Size; use Image::Imlib2; my $target; # 対象ディレクトリ.この直下に画像ファイルがあるとする my $manifest; # マニフェスト情報 my @image; # 画像 my @css; # CSS
$manifest = do "${target}/_manifest.pl" or die $!;
な感じで読み込みます.
ちなみにこの手法,このエントリを書いたり動作確認したりするためにcloneした App::AdventCalendarの中を見て初めて知りました.
マニフェストで対象になっている画像について,順に見ていきます.この段階で,各「段」における画像の幅の合計値や最大高さを基に,連結画像のサイズを計算したりします.
my ($sprite_width, $sprite_height) = (0, 0); # 連結画像の幅・高さ my ($x, $y) = (0, 0); my $w_sum_max = 0; # 各「段」の画像幅合計値のうち最大の物 for my $l (@{ $manifest->{images} }) { $x = 0; my $w_sum_in_line = 0; # 「段」における画像幅の合計値 my $h_max_in_line = 0; # 「段」における画像高さの最大値 while (1) { my ($pre, $sel) = (shift @$l, shift @$l); last unless defined $pre; my $imgfile = "${target}/${pre}.png"; # 画像のサイズを取得する my ($w, $h) = imgsize($imgfile); # Image::Imlib2オブジェクトを作っておく my $img = Image::Imlib2->load($imgfile); push @image, { img => $img, w => $w, h => $h, x => $x, y => $y }; # CSS: ここでbackground-positionの情報がわかる my $selector = ...; # 省略 my $css = sprintf( '%s { background-position %dpx %dpx; }', $selector, -$x, -$y, ); push @$css, $css; $h_max_in_line = $h if $h > $h_max_in_line; $x += $w; } $w_sum_max = $w_sum_in_line if $w_sum_in_line > $w_sum_max; $y += $h_max_in_line; } ($sprite_width, $sprite_height) = ($w_sum_max, $y);
連結画像のサイズや,連結対象の各画像をどのように連結するかの情報が揃ったので,これらを基に,実際に画像処理を行い,最後に出力します.
my $img_sprite = Image::Imlib2->new($sprite_width, $sprite_height); for my $i (@image) { # 対象画像1つ1つを,連結画像に合成していく $img_sprite->blend( $i->{img}, 1, 0, 0, # 対象画像のどの矩形領域を合成するか:始点(左上) $i->{w}, $i->{h}, # 対象画像のどの矩形領域を合成するか:始点からの幅・高さ $i->{x}, $i->{y}, # 連結画像のどの矩形領域へ合成するか:始点(左上) $i->{w}, $i->{h}, # 連結画像のどの矩形領域へ合成するか:始点からの幅・高さ ); }
あとはできあがった画像を出力するだけ.
$img_sprite->save('/path/to/csssprite.png');
今回定義したマニフェストの下では,次のように生成されます.
残るはCSSです.
if (defined $manifest->{base}) { # $manifest->{base}{selector}, $manifest->{base}{props} # を基にCSSの書式に整形する my $css = ... unshift @css, $css; } my $css_out = join "\n", @css; open my $fh, '>', '/path/to/csssprice.css' or die $!; print $fh, $css_out; close $fh;
一部,というかけっこう端折りましたが,今回のマニフェストの下では,次のように生成されます.
#navi ul li a { background-image: url(./csssprite.png); display: block; width: 160px; height: 50px; } #navi ul li#a1 a { background-position: 0px 0px; } #navi ul li#b1 a { background-position: -160px 0px; } #navi ul li#c1 a { background-position: -320px 0px; } #navi ul li#d1 a { background-position: -480px 0px; } #navi ul li#a1 a:hover { background-position: 0px -50px; } #navi ul li#b1 a:hover { background-position: -160px -50px; } #navi ul li#c1 a:hover { background-position: -320px -50px; } #navi ul li#d1 a:hover { background-position: -480px -50px; } #navi ul li#e1 a { background-position: 0px -100px; } #navi ul li#f1 a { background-position: -160px -100px; } #navi ul li#g1 a { background-position: -320px -100px; } #navi ul li#h1 a { background-position: -480px -100px; } #navi ul li#e1 a:hover { background-position: 0px -150px; } #navi ul li#f1 a:hover { background-position: -160px -150px; } #navi ul li#g1 a:hover { background-position: -320px -150px; } #navi ul li#h1 a:hover { background-position: -480px -150px; }
以上のような処理をモジュール化してみたものを,「Image::CSSSprite」としてgithubに上げてみました.上記で省略した部分については,そちらの中身をご覧いただければ幸いです.(時間の都合上,未テストですが!)
Image::CSSSpriteを使ったスクリプトの例です.
# /path/to/img/_manifest.pl を読む % /path/to/script.pl --target /path/to/imgs --img csssprite.png --css csssprite.css # 任意のマニフェストを読む % /path/to/script.pl --target /path/to/imgs --manifest /path/to/manifest.pl --img csssprite.png --css csssprite.css
といった感じで,コマンドラインでCSS Spriteできます.
#!/usr/bin/env perl use strict; use warnings; use FindBin; use lib "${FindBin::Bin}/../lib"; use Image::CSSSprite; use Try::Tiny; use opts; my ($target, $manifest, $img_out, $css_out); try { opts $target => { isa => 'Str', required => 1 }, $manifest => { isa => 'Str' }, $img_out => { isa => 'Str', alias => 'img|image' }, $css_out => { isa => 'Str', alias => 'css' }, ; } catch { usage(shift); exit 1; }; sub usage { chomp( my $msg = shift || '' ); my $__FILE__ = __FILE__; print << " ..."; Error: $msg Usage: $__FILE__ --target /path/to/imgs --img /path/to/csssprited.png --css /path/to/csssprited.css Options: ... } sub main { my $csssp = Image::CSSSprite->new({ target => $target, manifest => $manifest, img_out => $img_out, css_out => $css_out, }); $csssp->manifest_from_script; $csssp->scan_images; print $csssp->css; $csssp->save; } main();
以上,Image::SizeとImage::Imlib2を使ってCSS Spriteなことをするための一手法について紹介させていただきました.最後まで読んでいただいた方は,ありがとうございました.
さーて,明日のカジュアルさんは...zentoooさんです.お楽しみに,でゲソ!