CSS-SpriteMaker
view release on metacpan or search on metacpan
lib/CSS/SpriteMaker.pm view on Meta::CPAN
package CSS::SpriteMaker;
use strict;
use warnings;
use File::Find;
use Image::Magick;
use List::Util qw(max);
use Module::Pluggable
search_path => ['CSS::SpriteMaker::Layout'],
except => qr/CSS::SpriteMaker::Layout::Utils::.*/,
require => 1,
inner => 0;
use POSIX qw(ceil);
=head1 NAME
CSS::SpriteMaker - Combine several images into a single CSS sprite
=head1 VERSION
Version 1.01
=cut
our $VERSION = '1.01';
=head1 SYNOPSIS
use CSS::SpriteMaker;
my $SpriteMaker = CSS::SpriteMaker->new(
verbose => 1, # optional
#
# Options that impact the lifecycle of css class name generation
#
# if provided will replace the default logic for creating css classnames
# out of image filenames. This filename-to-classname is the FIRST step
# of css classnames creation. It's safe to return invalid css characters
# in this subroutine. They will be cleaned up internally.
#
rc_filename_to_classname => sub { my $filename = shift; ... } # optional
# ... cleaning stage happens (all non css safe characters are removed)
# This adds a prefix to all the css class names. This is called after
# the cleaning stage internally. Don't mess with invalid CSS characters!
#
css_class_prefix => 'myicon-',
# This is the last step. Change here whatever part of the final css
# class name.
#
rc_override_classname => sub { my $css_class = shift; ... } # optional
);
$SpriteMaker->make_sprite(
source_images => ['/path/to/imagedir', '/images/img1.png', '/img2.png'];
target_file => '/tmp/test/mysprite.png',
layout_name => 'Packed', # optional
remove_source_padding => 1, # optional
enable_colormap => 1, # optional
lib/CSS/SpriteMaker.pm view on Meta::CPAN
);
Default values are set to:
=over 4
=item remove_source_padding : false,
=item verbose : false,
=item enable_colormap : false,
=item format : png,
=item css_class_prefix : ''
=back
The parameter rc_filename_to_classname is a code reference to a function that
allow to customize the way class names are generated. This function should take
one parameter as input and return a class name
=cut
sub new {
my $class = shift;
my %opts = @_;
# defaults
$opts{remove_source_padding} //= 0;
$opts{add_extra_padding} //= 0;
$opts{verbose} //= 0;
$opts{format} //= 'png';
$opts{layout_name} //= 'Packed';
$opts{css_class_prefix} //= '';
$opts{enable_colormap} //= 0;
my $self = {
css_class_prefix => $opts{css_class_prefix},
source_images => $opts{source_images},
source_dir => $opts{source_dir},
target_file => $opts{target_file},
is_verbose => $opts{verbose},
format => $opts{format},
remove_source_padding => $opts{remove_source_padding},
enable_colormap => $opts{enable_colormap},
add_extra_padding => $opts{add_extra_padding},
output_css_file => $opts{output_css_file},
output_html_file => $opts{output_html_file},
# layout_name is used as default
layout => {
name => $opts{layout_name},
# no options by default
options => {}
},
rc_filename_to_classname => $opts{rc_filename_to_classname},
rc_override_classname => $opts{rc_override_classname},
# the maximum color value
color_max => 2 ** Image::Magick->QuantumDepth - 1,
};
return bless $self, $class;
}
=head2 compose_sprite
Compose many sprite layouts into one sprite. This is done by applying
individual layout separately, then merging the final result together using a
glue layout.
my $is_error = $SpriteMaker->compose_sprite (
parts => [
{ source_images => ['some/file.png', 'path/to/some_directory'],
layout_name => 'Packed',
},
{ source_images => ['path/to/some_directory'],
layout => {
name => 'DirectoryBased',
}
include_in_css => 0, # optional
remove_source_padding => 1, # optional (defaults to 0)
enable_colormap => 1, # optional (defaults to 0)
add_extra_padding => 40, # optional, px (defaults to 0px)
},
],
# arrange the previous two layout using a glue layout
layout => {
name => 'FixedDimension',
dimension => 'horizontal',
n => 2
}
target_file => 'sample_sprite.png',
format => 'png8', # optional, default is png
);
Note the optional include_in_css option, which allows to exclude a group of
images from the CSS (still including them in the resulting image).
=cut
sub compose_sprite {
my $self = shift;
my %options = @_;
if (exists $options{layout}) {
return $self->_compose_sprite_with_glue(%options);
}
else {
return $self->_compose_sprite_without_glue(%options);
}
}
=head2 make_sprite
Creates the sprite file out of the specifed image files or directories, and
according to the given layout name.
my $is_error = $SpriteMaker->make_sprite(
source_images => ['some/file.png', path/to/some_directory],
lib/CSS/SpriteMaker.pm view on Meta::CPAN
#
# Still no layout, check the cache!
#
if (!defined $Layout) {
# try checking in the cache before giving up...
if (exists $self->{_cache_layout}
&& defined $self->{_cache_layout}) {
$Layout = $self->{_cache_layout};
}
}
#
# Still nothing, then use default layout
#
if (!defined $Layout) {
my $layout_name = $self->{layout}{name};
my $layout_options = {};
LOAD_DEFAULT_LAYOUT_PLUGIN:
for my $plugin ($self->plugins()) {
my ($plugin_name) = reverse split "::", $plugin;
if ($plugin eq $layout_name || $plugin_name eq $layout_name) {
$self->_verbose(" * using DEFAULT layout $plugin");
$Layout = $plugin->new($options{rh_sources_info}, $layout_options);
last LOAD_DEFAULT_LAYOUT_PLUGIN;
}
}
}
return $Layout;
}
sub _write_image {
my $self = shift;
my %options = @_;
my $target_file = $options{target_file} // $self->{target_file};
my $output_format = $options{format} // $self->{format};
my $Layout = $options{Layout} // 0;
my $rh_sources_info = $options{rh_sources_info} // 0;
if (!$target_file) {
die "the ``target_file'' parameter is required for this subroutine or you must instantiate Css::SpriteMaker with the target_file parameter";
}
if (!$rh_sources_info) {
die "The 'rh_sources_info' parameter must be passed to _write_image";
}
if (!$Layout) {
die "The 'layout' parameter needs to be specified for _write_image, and must be a CSS::SpriteMaker::Layout object";
}
$self->_verbose(" * writing sprite image");
$self->_verbose(sprintf("Target image size: %s, %s",
$Layout->width(),
$Layout->height())
);
my $Target = Image::Magick->new();
$Target->Set(size => sprintf("%sx%s",
$Layout->width(),
$Layout->height()
));
# prepare the target image
if (my $err = $Target->ReadImage('xc:white')) {
warn $err;
}
$Target->Set(type => 'TruecolorMatte');
# make it transparent
$self->_verbose(" - clearing canvas");
$Target->Draw(
fill => 'transparent',
primitive => 'rectangle',
points => sprintf("0,0 %s,%s", $Layout->width(), $Layout->height())
);
$Target->Transparent('color' => 'white');
# place each image according to the layout
ITEM_ID:
for my $source_id ($Layout->get_item_ids) {
my $rh_source_info = $rh_sources_info->{$source_id};
my ($layout_x, $layout_y) = $Layout->get_item_coord($source_id);
$self->_verbose(sprintf(" - placing %s (format: %s size: %sx%s position: [%s,%s])",
$rh_source_info->{pathname},
$rh_source_info->{format},
$rh_source_info->{width},
$rh_source_info->{height},
$layout_y,
$layout_x
));
my $I = Image::Magick->new();
my $err = $I->Read($rh_source_info->{pathname});
if ($err) {
warn $err;
next ITEM_ID;
}
my $padding = $rh_source_info->{add_extra_padding};
my $destx = $layout_x + $padding;
my $desty = $layout_y + $padding;
$Target->Composite(image=>$I,compose=>'xor',geometry=>"+$destx+$desty");
}
# write target image
my $err = $Target->Write("$output_format:".$target_file);
if ($err) {
warn "unable to obtain $target_file for writing it as $output_format. Perhaps you have specified an invalid format. Check http://www.imagemagick.org/script/formats.php for a list of supported formats. Error: $err";
$self->_verbose("Wrote $target_file");
return 1;
}
# cache the layout and the rh_info structure so that it hasn't to be passed
# as a parameter next times.
$self->{_cache_layout} = $Layout;
# cache the target image file, as making the stylesheet can't be done
# without this information.
$self->{_cache_target_image_file} = $target_file;
# cache sources info
$self->{_cache_rh_source_info} = $rh_sources_info;
return 0;
}
=head2 _get_image_properties
Return an hashref of information about the image at the given pathname.
=cut
sub _get_image_properties {
my $self = shift;
my $image_path = shift;
my $remove_source_padding = shift;
my $add_extra_padding = shift;
my $enable_colormap = shift;
my $Image = Image::Magick->new();
my $err = $Image->Read($image_path);
if ($err) {
warn $err;
return {};
}
my $rh_info = {};
$rh_info->{first_pixel_x} = 0,
$rh_info->{first_pixel_y} = 0,
$rh_info->{width} = $Image->Get('columns');
$rh_info->{height} = $Image->Get('rows');
$rh_info->{comment} = $Image->Get('comment');
$rh_info->{colors}{total} = $Image->Get('colors');
$rh_info->{format} = $Image->Get('magick');
if ($remove_source_padding) {
#
# Find borders for this image.
#
# (RE-)SET:
# - first_pixel(x/y) as the true point the image starts
# - width/height as the true dimensions of the image
#
my $w = $rh_info->{width};
my $h = $rh_info->{height};
# seek for left/right borders
my $first_left = 0;
my $first_right = $w-1;
my $left_found = 0;
my $right_found = 0;
BORDER_HORIZONTAL:
for my $x (0 .. ceil(($w-1)/2)) {
my $xr = $w-$x-1;
for my $y (0..$h-1) {
my $al = $Image->Get(sprintf('pixel[%s,%s]', $x, $y));
my $ar = $Image->Get(sprintf('pixel[%s,%s]', $xr, $y));
# remove rgb info and only get alpha value
$al =~ s/^.+,//;
$ar =~ s/^.+,//;
if ($al != $self->{color_max} && !$left_found) {
$first_left = $x;
$left_found = 1;
}
if ($ar != $self->{color_max} && !$right_found) {
$first_right = $xr;
$right_found = 1;
}
last BORDER_HORIZONTAL if $left_found && $right_found;
}
}
$rh_info->{first_pixel_x} = $first_left;
$rh_info->{width} = $first_right - $first_left + 1;
# seek for top/bottom borders
my $first_top = 0;
( run in 2.039 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )