MTAssetThumbnailLinkタグとMTAssetThumbnailURLタグのsquareモディファイアの動作
MTAssetThumbnailLinkタグとMTAssetThumbnailURLタグのsquareモディファイアの動作について調べてみました。調査の発端はriatwさんのツイートです。
1.squareモディファイアについて
squareモディファイアは、サムネイルの縦横比を1:1で出力するためのものです。辺のサイズは widthモディファイア, heightモディファイアまたはscale モディファイアで決定します。
冒頭に記したとおり、このモディファイアはMTAssetThumbnailLinkタグ、MTAssetThumbnailURLタグで利用できます。
2.サンプル
上がsquareモディファイアで整形した画像、下が元画像です。どの部分を切り取っているかは次項で解説します。
3.squareモディファイアで切り取られる部分
squareモディファイアでは、まず次の2つの値を元に、1辺のサイズ(縮小したサイズではなく、縦横比を1:1にするためのサイズ)とx方向およびy方向の切り取り開始位置を決定します。
- 元画像の幅(px):w
- 元画像の高さ(px):h
wがhより大きい場合は次のように計算します。
- 1辺のサイズ:h
- x方向の切り取り開始位置:w-h/2(1辺が画像の中心になるようにxを設定)
- y方向の切り取り開始位置:0
wがhより小さい場合(またはwとhが等しい場合)は次のように計算します。
- 1辺のサイズ:w
- x方向の切り取り開始位置:0
- y方向の切り取り開始位置:h-w/2(1辺が画像の中心になるようにyを設定)
その後、正方形に切り取った画像を、widthモディファイア、heightモディファイア、scaleモディファイアで指定されたサイズに縮小します。拡大はできません。
4.処理の流れ
おまけで、トレース結果を残しておきます。
MTAssetThumbnailURLタグはMT::Template::Tags::Asset::_hdlr_asset_thumbnail_urlを実行します。
sub _hdlr_asset_thumbnail_url {
my ($ctx, $args) = @_;
my $a = $ctx->stash('asset')
or return $ctx->_no_asset_error();
return '' unless $a->has_thumbnail;
my %arg;
foreach (keys %$args) {
$arg{$_} = $args->{$_};
}
$arg{Width} = $args->{width} if $args->{width};
$arg{Height} = $args->{height} if $args->{height};
$arg{Scale} = $args->{scale} if $args->{scale};
$arg{Square} = $args->{square} if $args->{square};
my ($url, $w, $h) = $a->thumbnail_url(%arg);
return $url || '';
}
MTAssetThumbnailLinkタグはMT::Template::Tags::Asset::_hdlr_asset_thumbnail_linkを実行します。いずれも、後は青色で示す関数を順次実行します(多分)。
sub _hdlr_asset_thumbnail_link {
my ($ctx, $args) = @_;
my $a = $ctx->stash('asset')
or return $ctx->_no_asset_error();
my $class = ref($a);
return '' unless UNIVERSAL::isa($a, 'MT::Asset::Image');
# # Load MT::Image
# require MT::Image;
# my $img = new MT::Image(Filename => $a->file_path)
# or return $ctx->error(MT->translate(MT::Image->errstr));
# Get dimensions
my %arg;
$arg{Width} = $args->{width} if $args->{width};
$arg{Height} = $args->{height} if $args->{height};
$arg{Scale} = $args->{scale} if $args->{scale};
$arg{Square} = $args->{square} if $args->{square};
my ($url, $w, $h) = $a->thumbnail_url(%arg);
my $ret = sprintf qq(<a href="%s"), $a->url;
if ($args->{new_window}) {
$ret .= qq( target="_blank");
}
$ret .= sprintf qq(><img src="%s" width="%d" height="%d" alt="" /></a>), $url, $w, $h;
$ret;
}
MT::Asset::thumbnail_urlを実行します。
sub thumbnail_url {
my $asset = shift;
my (%param) = @_;
require File::Basename;
if (my ($thumbnail_file, $w, $h) = $asset->thumbnail_file(@_)) {
return $asset->stock_icon_url(@_) if !defined $thumbnail_file;
my $file = File::Basename::basename($thumbnail_file);
my $asset_file_path = $asset->SUPER::file_path();
my $site_url;
my $blog = $asset->blog;
if (!$blog) {
$site_url = $param{Pseudo} ? '%s' : MT->instance->support_directory_url;
}
elsif ( $asset_file_path =~ m/^%a/ ) {
$site_url = $param{Pseudo} ? '%a' : $blog->archive_url;
}
else {
$site_url = $param{Pseudo} ? '%r' : $blog->site_url;
}
if ($file && $site_url) {
require MT::Util;
my $path = $param{Path};
if (!defined $path) {
$path = MT::Util::caturl(MT->config('AssetCacheDir'), unpack('A4A2', $asset->created_on));
} else {
require File::Spec;
my @path = File::Spec->splitdir($path);
$path = '';
for my $p (@path) {
$path = MT::Util::caturl($path, $p);
}
}
$file = MT::Util::encode_url($file);
$site_url = MT::Util::caturl($site_url, $path, $file);
return ($site_url, $w, $h);
}
}
# Use a stock icon
return $asset->stock_icon_url(@_);
}
MT::Asset::Image::thumbnail_file(MT::Assetを継承)を実行します。
sub thumbnail_file {
my $asset = shift;
my (%param) = @_;
my $fmgr;
my $blog = $param{Blog} || $asset->blog;
require MT::FileMgr;
$fmgr ||= $blog ? $blog->file_mgr : MT::FileMgr->new('Local');
return undef unless $fmgr;
my $file_path = $asset->file_path;
return undef unless $fmgr->exists( $file_path );
require MT::Util;
my $asset_cache_path = $asset->_make_cache_path($param{Path});
my ( $i_h, $i_w ) = ( $asset->image_height, $asset->image_width );
return undef unless $i_h && $i_w;
# Pretend the image is already square, for calculation purposes.
if ($param{Square}) {
require MT::Image;
my %square = MT::Image->inscribe_square(
Width => $i_w, Height => $i_h );
($i_h, $i_w) = @square{qw( Size Size )};
if ( $param{Width} && !$param{Height} ) {
$param{Height} = $param{Width};
}
elsif ( !$param{Width} && $param{Height} ) {
$param{Width} = $param{Height};
}
}
if ( my $scale = $param{Scale} ) {
$param{Width} = int( ( $i_w * $scale ) / 100 );
$param{Height} = int( ( $i_h * $scale ) / 100 );
}
if ( !exists $param{Width} && !exists $param{Height} ) {
$param{Width} = $i_w;
$param{Height} = $i_h;
}
# find the longest dimension of the image:
my ( $n_h, $n_w ) =
_get_dimension( $i_h, $i_w, $param{Height}, $param{Width} );
my $file = $asset->thumbnail_filename(%param) or return;
my $thumbnail = File::Spec->catfile( $asset_cache_path, $file );
# thumbnail file exists and is dated on or later than source image
if ( $fmgr->exists($thumbnail)
&& ( $fmgr->file_mod_time($thumbnail) >= $fmgr->file_mod_time($file_path))) {
return ( $thumbnail, $n_w, $n_h );
}
# stale or non-existent thumbnail. let's create one!
return undef unless $fmgr->can_write($asset_cache_path);
my $data;
if ( ( $n_w == $i_w ) && ( $n_h == $i_h ) && !$param{Square}
&& !$param{Type} ) {
$data = $fmgr->get_data( $file_path, 'upload' );
}
else {
# create a thumbnail for this file
require MT::Image;
my $img = new MT::Image( Filename => $file_path )
or return $asset->error( MT::Image->errstr );
# Really make the image square, so our scale calculation works out.
if ($param{Square}) {
($data) = $img->make_square()
or return $asset->error(
MT->translate( "Error cropping image: [_1]", $img->errstr ) );
}
($data) = $img->scale( Height => $n_h, Width => $n_w )
or return $asset->error(
MT->translate( "Error scaling image: [_1]", $img->errstr ) );
if (my $type = $param{Type}) {
($data) = $img->convert( Type => $type )
or return $asset->error(
MT->translate( "Error converting image: [_1]", $img->errstr ) );
}
}
$fmgr->put_data( $data, $thumbnail, 'upload' )
or return $asset->error(
MT->translate( "Error creating thumbnail file: [_1]", $fmgr->errstr ) );
return ( $thumbnail, $n_w, $n_h );
}
MT::Image::inscribe_squareを実行します。
sub inscribe_square {
my $class = shift;
my %params = @_;
my ($w, $h) = @params{qw( Width Height )};
my ($dim, $x, $y);
if ($w > $h) {
$dim = $h;
$x = int(($w - $dim) / 2);
$y = 0;
}
else {
$dim = $w;
$x = 0;
$y = int(($h - $dim) / 2);
}
return ( Size => $dim, X => $x, Y => $y );
}
MT::Image::make_squareを実行します。
sub make_square {
my $image = shift;
my %square = $image->inscribe_square(
Width => $image->{width},
Height => $image->{height},
);
$image->crop(%square);
}
このあとは利用している画像ライブラリによって、実行関数が異なります。
ImageMagickを利用している場合、MT::Image::ImageMagick::cropを実行します。
sub crop {
my $image = shift;
my %param = @_;
my ($size, $x, $y) = @param{qw( Size X Y )};
my $magick = $image->{magick};
my $err = $magick->Crop(width => $size, height => $size, x => $x, y => $y);
return $image->error(MT->translate(
"Cropping a [_1]x[_1] square at [_2],[_3] failed: [_4]", $size, $x,
$y, $err)) if $err;
## Remove page offsets from the original image, per this thread:
## http://studio.imagemagick.org/pipermail/magick-users/2003-September/010803.html
$magick->Set( page => '+0+0' );
($image->{width}, $image->{height}) = ($size, $size);
wantarray ? ($magick->ImageToBlob, $size, $size) : $magick->ImageToBlob;
}
NetPBMを利用している場合、MT::Image::NetPBM::cropを実行します。
sub crop {
my $image = shift;
my %param = @_;
my ($size, $x, $y) = @param{qw( Size X Y )};
my($w, $h) = $image->get_dimensions(@_);
my $type = $image->{type};
my($out, $err);
my $pbm = $image->_find_pbm or return;
my @in = ("$pbm${type}topnm", ($image->{data} ? () : $image->{file} ? $image->{file} : ()));
my @crop = ("${pbm}pnmcut", $x, $y, $size, $size);
my @out;
for my $try (qw( ppm pnm )) {
my $prog = "${pbm}${try}to$type";
@out = ($prog), last if -x $prog;
}
my(@quant);
if ($type eq 'gif') {
push @quant, ([ "${pbm}ppmquant", 256 ], '|');
}
IPC::Run::run(\@in, '<', ($image->{data} ? \$image->{data} : \undef), '|',
\@crop, '|',
@quant,
\@out, \$out, \$err)
or return $image->error(MT->translate(
"Cropping to [_1]x[_1] failed: [_2]", $size, $err));
($image->{width}, $image->{height}, $image->{data}) = ($w, $h, $out);
wantarray ? ($out, $w, $h) : $out;
}
GDを利用している場合、MT::Image::GD::cropを実行します。
sub crop {
my $image = shift;
my %param = @_;
my ($size, $x, $y) = @param{qw( Size X Y )};
my $src = $image->{gd};
my $gd = GD::Image->new($size, $size, 1); # True color image (24 bit)
$gd->copy($src, 0, 0, $x, $y, $size, $size);
($image->{gd}, $image->{width}, $image->{height}) = ($gd, $size, $size);
wantarray ? ($image->blob, $size, $size) : $image->blob;
}
以上です。
- Movable Typeで特定の拡張子のファイルをアップロードする方法
- 特定のアイテムのみをブログ記事に表示する
- Movable Typeのブログ記事で未使用のアイテム一覧を表示する(本文未挿入版)
- Movable Typeの管理サイトでFTPが使えないときにファイルをアップロードする裏ワザ
- Movable Typeのブログ記事で未使用のアイテム一覧を表示する
- サムネイルリストから不要な右マージンを除去する
- Movable Type 5 でスライドショーを実現する(その2:応用)
- Movable Type のブログ記事に表示する画像のサイズを制御する
- Movable Type + Windows で日本語ファイル名を扱う方法
- Movable Type 4 におけるアイテムのアップロード動作(その2)
- Movable Type 4 におけるアイテムのアップロード動作(その1)
- ブログ記事に挿入したアイテムだけのサムネイル画像一覧を表示する(その2)
- ブログ記事に挿入したアイテムだけのサムネイル画像一覧を表示する