Image-APNG
view release on metacpan or search on metacpan
lib/Image/APNG.pm view on Meta::CPAN
package Image::APNG ;
use strict ;
use warnings ;
use Image::Magick ;
our $VERSION = '1.0.0' ;
=head1 NAME
Image::APNG - Generate Animated PNG (APNG) files from individual PNG images
=head1 SYNOPSIS
use Image::APNG;
my $frames =
[
['frame1.png', 100],
['frame2.png', 150],
['frame3.png', 100]
] ;
my $options =
{
loop_count => 0,
normalize_resolution => 1,
background_color => [255, 255, 255, 0]
} ;
my $result = Image::APNG::generate($frames, $options) ;
if ($result->{status} == 0)
{
open my $fh, '>', 'output.png' ;
binmode $fh ;
print $fh $result->{data} ;
close $fh ;
}
else
{
print "Errors: " . join("\n", @{$result->{errors}}) ;
}
=head1 DESCRIPTION
Generates APNG files from a list of PNG images with individual frame delays.
=cut
#----------------------------------------------------------------------------------------------
sub generate
{
my ($frames, $options) = @_ ;
$options //= {} ;
my $errors = [] ;
my $default_options =
{
optimize_palette => 0,
normalize_resolution => 0,
target_resolution => undef,
background_color => [0, 0, 0, 0],
loop_count => 0,
disposal_method => 1,
blend_operation => 1
} ;
$options = {%$default_options, %$options} ;
return {status => 1, errors => ['No frames provided'], data => undef} unless $frames && @$frames ;
my $loaded_frames = load_frames($frames, $errors, $options) ;
return {status => 1, errors => $errors, data => undef} unless @$loaded_frames ;
$loaded_frames = normalize_frames($loaded_frames, $options, $errors) if $options->{normalize_resolution} ;
$loaded_frames = optimize_palettes($loaded_frames, $errors) if $options->{optimize_palette} ;
return
{
status => @$errors ? 1 : 0,
errors => $errors,
data => assemble_apng($loaded_frames, $options, $errors),
} ;
}
#----------------------------------------------------------------------------------------------
sub load_frames
{
my ($frames, $errors, $options) = @_ ;
my ($loaded, $previous_valid) = ([]) ;
for my $frame_data (@$frames)
{
my ($filename, $delay_ms) = @$frame_data ;
my $image = Image::Magick->new() ;
my $status = $image->Read($filename) ;
if ($status)
{
push @$errors, "Failed to load $filename: $status" ;
if ($previous_valid)
{
my $blank = $previous_valid->Clone() ;
$blank->Quantize(colorspace => 'Transparent') ;
push @$loaded,
{
image => $blank,
delay => $delay_ms,
width => $previous_valid->Get('width'),
height => $previous_valid->Get('height')
} ;
}
else
{
push @$errors, "Cannot create blank frame: no previous valid frame" ;
}
next ;
}
my $width = $image->Get('width') ;
my $height = $image->Get('height') ;
push @$loaded,
{
image => $image,
delay => $delay_ms,
width => $width,
height => $height
} ;
$previous_valid = $image ;
}
return $loaded ;
}
#----------------------------------------------------------------------------------------------
sub normalize_frames
{
my ($frames, $options, $errors) = @_ ;
my ($max_width, $max_height) ;
if ($options->{target_resolution})
{
($max_width, $max_height) = @{$options->{target_resolution}} ;
}
else
{
$max_width = 0 ;
$max_height = 0 ;
for my $frame (@$frames)
{
$max_width = $frame->{width} if $frame->{width} > $max_width ;
$max_height = $frame->{height} if $frame->{height} > $max_height ;
}
}
my $bg_color = $options->{background_color} ;
my $bg_string = sprintf
(
'rgba(%d,%d,%d,%f)',
$bg_color->[0],
$bg_color->[1],
$bg_color->[2],
$bg_color->[3] / 255.0
) ;
for my $frame (@$frames)
{
next if $frame->{width} == $max_width && $frame->{height} == $max_height ;
my $canvas = Image::Magick->new(size => "${max_width}x${max_height}") ;
$canvas->Read("xc:$bg_string") ;
my $x_offset = int(($max_width - $frame->{width}) / 2) ;
my $y_offset = int(($max_height - $frame->{height}) / 2) ;
$canvas->Composite
(
image => $frame->{image},
x => $x_offset,
y => $y_offset,
compose => 'Over'
) ;
$frame->{image} = $canvas ;
$frame->{width} = $max_width ;
$frame->{height} = $max_height ;
}
return $frames ;
}
#----------------------------------------------------------------------------------------------
sub optimize_palettes
{
my ($frames, $errors) = @_ ;
for my $frame (@$frames)
{
my $img = $frame->{image} ;
my $status = $img->Quantize
(
colors => 256,
colorspace => 'RGB',
dither => 'True',
treedepth => 8,
) ;
push @$errors, "Palette optimization failed: $status" if $status ;
$img->Set(type => 'Palette') ;
}
return $frames ;
}
#----------------------------------------------------------------------------------------------
sub assemble_apng
{
my ($frames, $options, $errors) = @_ ;
my $first_frame = $frames->[0] ;
my $width = $first_frame->{width} ;
my $height = $first_frame->{height} ;
my $png_signature = pack('C8', 137, 80, 78, 71, 13, 10, 26, 10) ;
my $ihdr = create_ihdr($width, $height, $first_frame->{image}) ;
( run in 2.372 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )