Automate-Animate-FFmpeg

 view release on metacpan or  search on metacpan

lib/Automate/Animate/FFmpeg.pm  view on Meta::CPAN

package Automate::Animate::FFmpeg;

use 5.006;
use strict;
use warnings;

our $VERSION = '0.13';

use utf8; # filenames can be in utf8

binmode STDERR, ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

use File::Temp;
use File::Find::Rule;
use IPC::Run;
use Text::ParseWords;
use Encode;
use Data::Roundtrip qw/perl2dump no-unicode-escape-permanently/;
use Cwd::utf8 qw/abs_path cwd/;

sub	new {
	my $class = $_[0];
	my $params = $_[1];

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $self = {
		'input-images' => [],
		'output-filename' => undef,
		'verbosity' => 0,
		# params to ffmpeg must be given as an arrayref
		# these are our standard params, they are read only and must
		# be supplied here as an ARRAYref
		'ffmpeg-standard-params' => ['-c', 'copy', '-c:v', 'copy', '-c:a', 'copy', '-q:a', '0', '-q:v', '1'],
		# extra params to ffmpeg can be specified by the caller
		# we store this as an ARRAYref, each option and value is an item of its own
		'ffmpeg-extra-params' => [],
		# the duration of each frame in seconds (fractional supported)
		# or <=0 for using FFmpeg's defaults
		'frame-duration' => 0,
		# this will be modified during perl Makefile.PL
		'ffmpeg-executable' => '/usr/local/bin/ffmpeg', # specify fullpath if not in path
		# end this will be modified during perl Makefile.PL
	};
	bless $self, $class;

	my $ak;

	# sort out the easy params
	my $verbos = 0;
	$ak = 'verbosity';
	if( exists($params->{$ak}) && defined($verbos=$params->{$ak}) ){ $self->verbosity($verbos) }

	for $ak (qw/
		output-filename
		frame-duration
	/){
		if( exists($params->{$ak}) && defined($params->{$ak}) ){
			$self->{$ak} = $params->{$ak};
			if( $verbos > 1 ){ print "${whoami} (via $parent), line ".__LINE__." : parameter '$ak' set to : ".$params->{$ak}."\n"; }
		}
	}

	# specify input images as scalar, arrayref or hashref (values)
	$ak = 'input-images';
	if( exists($params->{$ak}) && defined($params->{$ak})
		&& scalar($params->{$ak})
	){
		$self->input_images($params->{$ak});
	}

	# input image filenames are read from specified file (one filename per line)
	$ak = 'input-images-from-file';
	if( exists($params->{$ak}) && defined($params->{$ak})
		&& scalar($params->{$ak})
	){
		if( ! $self->input_file_with_images($params->{$ak}) ){ print STDERR perl2dump($params->{$ak})."${whoami} (via $parent), line ".__LINE__." : error, failed to load input images from file containing their pathnames: '".$params->{$ak}."'.\n"; return un...

lib/Automate/Animate/FFmpeg.pm  view on Meta::CPAN

	return undef
}
sub	ffmpeg_extra_params {
	my $self = $_[0];
	my $m = $_[1];
	return $self->{'ffmpeg-extra-params'} unless defined $m;
	my $ret = _cmdline2argsarray($self->{'ffmpeg-extra-params'});
	if( ! defined $ret ){ print STDERR perl2dump($ret)."ffmpeg_extra_params() : error, failed to pass above arguments.\n"; return undef }
	$self->{'ffmpeg-extra-params'} = $ret;
	return $ret
}
sub	ffmpeg_standard_params { return $_[0]->{'ffmpeg-standard-params'} }
sub	num_input_images { return scalar @{$_[0]->{'input-images'}} }
# specify a text file which holds image filenames, one per line to be added
# hash-comments are understood, empty/only-space lines are removed
# returns 1 on success, 0 on failure
sub	input_file_with_images {
	my ($self, $infile) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $verbos = $self->verbosity();
	if( ! defined $infile ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, an input filename of input image filenames is expected.\n"; return 0 }
	my $fh;
	if( ! open($fh, '<:encoding(UTF-8)', $infile) ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, could not open input file '$infile' for reading, $!\n"; return 0 }
	while( <$fh> ){
		chomp;
		s/#.*$//;
		s/^\s*$//;
		$self->input_images($_) unless /^\s*$/;
	} close $fh;
	return 1
}
sub	clear_input_images { $#{ $_[0]->{'input-images'} } = -1 }
# Add using a single pattern/searchdir
# add image files via a pattern and an input dir, e.g. '*.png', '/x/y/z/'
# make sure that the order you expect is what you get during the pattern materialisation
# the search dir is optional, default is Cwd::cwd
sub	input_pattern {
	my ($self, $params) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $verbos = $self->verbosity();
	my ($_pattern, $indir) = @$params;
	my $indir_need_encode_utf8 = 0;
	if( ! defined $indir ){
		$indir = _my_cwd();
		if( $verbos > 0 ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : warning, no search dir was specified and using current dir '$indir'.\n" }
	} else { $indir_need_encode_utf8 = 1 }
	my $pattern;
	# allows for /pattern/modifiers
	if( $_pattern =~ m!^regex\(/(.*?)/(.*?)\)$! ){
		# see https://www.perlmonks.org/?node_id=1210675
		my $pa = $1; my $mo = $2;
		if( $mo!~/^[msixpodualn]+$/ ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, illegal modifiers ($mo) to the specified regex detected.\n"; return 0 }
		# the modifiers are entered as (?...) before the regex pattern
		$pattern = qr/(?${mo})${pa}/;
	} else { $pattern = $_pattern }
	if( $verbos > 1 ){ print "${whoami} (via $parent), line ".__LINE__." : searching under dir '$indir' with pattern '".$pattern."' ...\n" }

	if( ! defined $self->input_images([
		# this little piglet does not support unicode
		# or, rather, readdir() needs some patching
		# additionally, it fails in M$ as the unicoded
		# filenames get doubly encoded, let's see if this will fix it:
		($^O eq 'MSWin32')
			?
			    File::Find::Rule
				->file()
				->name($pattern)
				->in($indir)
			:
			    map { Encode::decode_utf8($_) }
			    File::Find::Rule
				->file()
				->name($pattern)
				->in(Encode::encode_utf8($indir))
	]) ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, call to input_images() has failed.\n"; return 0 }

	return 1 # success
}
# This adds many patterns:
# the input is an ARRAY of 1-or-2-item arrays
# each subarray must consist of a pattern and optionally a search dir (else current dir will be used)
sub	input_patterns {
	my ($self, $specs) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $verbos = $self->verbosity();
	for my $as (@$specs){
		if( (scalar(@$as)==0)
		 || (scalar(@$as)>2)
		){ print STDERR perl2dump($as)."${whoami} (via $parent), line ".__LINE__." : error, the spec must contain at least a pattern and optionally a search-dir as an array, see above.\n"; return 0; }
		if( ! $self->input_pattern($as) ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, call to input_pattern() has failed for this spec: @$as\n"; return 0 }
	}
	return 1 # success
}
# if no parameter is specified then it returns the current list of input images as an arrayref
# Otherwise:
# specify one or more input filenames (of images) via a single scalar, an arrayref or
# a hashref whose values are image filenames, to convert them into video
# in this case returns undef on failure or the current, updated list of input images on success
sub	input_images {
	my ($self, $m) = @_;
	if( ! defined $m ){ return $self->{'input-images'} }
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $verbos = $self->verbosity();
	if( $verbos > 0 ){
		if( $verbos > 1 ){ print STDOUT perl2dump($m)."${whoami} (via $parent), line ".__LINE__." : called with input images as shown above ...\n" }
		else { print STDOUT "${whoami} (via $parent), line ".__LINE__." : called ...\n" }
	}
	# NOTE: Cwd::abs_path() messes up on unicode filenames and requires Encode::decode_utf8()
	# but there is also Cwd::utf8 to consider sometime...
	my $rf = ref $m;
	if( $rf eq 'ARRAY' ){
		for my $af (@$m){
			if( ! -e $af ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : warning/1, input image '$af' does not exist on disk and will be ignored.\n"; next }
			push @{$self->{'input-images'}}, _my_abs_path($af);
		}
	} elsif( $rf eq 'HASH' ){
		for my $af (values %$m){
			if( ! -e $af ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : warning/2, input image '$af' does not exist on disk and will be ignored.\n"; next }
			push @{$self->{'input-images'}}, _my_abs_path($af);
		}		
	} elsif( $rf eq '' ){
		if( ! -e $m ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : warning/3, input image '$m' does not exist on disk and will be ignored.\n"; }
		else { push @{$self->{'input-images'}}, _my_abs_path($m) }
	} else { print STDERR "${whoami} (via $parent), line ".__LINE__." : error, input can be an arrayref of input image filenames, a hashref whose values are filenames or a single filename in the form of a scalar."; return undef }
	if( $verbos > 1 ){ print STDOUT perl2dump($self->{'input-images'})."${whoami} (via $parent), line ".__LINE__." : called and added some images, above is a list of all input images to create the animation.\n" }
	return $self->{'input-images'}
}

sub _my_cwd {
	return cwd()
}
# NOTE: unicode filenames may not be canonicalised
# e.g. iota-including-accent and iota with separate accent.
# the OS will not care but if you do comparisons you will fail
# So, consider canonicalising the filenames if you are doing comparison
# e.g. in the tests
# see https://perlmonks.org/?node_id=11156629
# Also, Cwd needs utf8 decode but Cwd::utf8 doesn't
# And File::Find::Rule needs also utf8 decoce unless it is chained to abs_path
sub _my_abs_path {
	#return Encode::decode_utf8(abs_path($_[0])) # for plain Cwd
	return abs_path($_[0]) # for Cwd::utf8
}

# that's the end, pod now starts

=pod

=encoding UTF-8

=head1 NAME

Automate::Animate::FFmpeg - Create animation from a sequence of images using FFmpeg

=head1 VERSION

Version 0.13


=head1 SYNOPSIS

This module creates an animation from a sequence of input
images using L<FFmpeg|https://ffmpeg.org>.
An excellent, open source program.

FFmpeg binaries must already be installed in your system.

    use Automate::Animate::FFmpeg;
    my $aaFFobj = Automate::Animate::FFmpeg->new({
      # specify input images in any of these 4 ways or a combination:
      # 1) by specifying each input image (in the order to appear)
      #    in an ARRAYref
      'input-images' => [
        '/xyz/abc/im1.png',
        '/xyz/abc/im2.png',
        ...
      ],
      # 2) by specifying an input pattern (glob or regex)
      #    and optional search path
      'input-pattern' => ['*.png', './'],
      # 3) by specifying an ARRAY of input patterns
      #    (see above)
      'input-patterns' => [
          ['*.tiff'],
          # specify a regex to filter-in all files under search dir
	  # NOTE: observe the escaping rules for each quotation method you use
          [qw!regex(/photos2023-.+?\\.png/i)!, 'abc/xyz'],
      ],
      # 4) by specifying a file which contains filenames
      #    of the input images.
      'input-images-from-file' => 'file-containing-a-list-of-pathnames-to-images.txt',

lib/Automate/Animate/FFmpeg.pm  view on Meta::CPAN

(specified using L<output_filename($m)> before the call.

Return value:

=over 4

=item * 0 on failure, 1 on success.

=back

=head2 C<input_images($m)>

  my $ret = $aaFF->input_images($m);

It sets or gets the list (as an ARRAYref) of all input images currently in the list
of images to create the animation. The optional input parameter, C<$m>,
is an ARRAYref of input images (their fullpath that is) to create
the animation.

Return value:

=over 4

=item * the list, as an ARRAYref, of the image filenames currently
set to create the animation.

=back

=head2 C<input_pattern($m)>

  $aaFF->input_pattern($m) or die "failed";

Initiates a search via L<File::Find::Rule> for the
input image files to create the animation using
the pattern C<$m-E<gt>[0]> with starting search dir being C<$m-E<gt>[1]>,
which is optional -- default being C<Cwd::cwd> (current working dir).
So, C<$m> is an array ref of one or two items. The first is the search
pattern and the optional second is the search path, defaulting to the current
working dir.

The pattern (C<$m-E<gt>[0]>) can be a shell wildcard, e.g. C<*.png>,
or a regex specified as C<regex(/REGEX-HERE/modifiers)>, for example
C<regex(/\.(mp3|ogg)$/i)> Both shell wildcards and regular expressions
must comply with what L<File::Find::Rule> expects, see [https://metacpan.org/pod/File::Find::Rule#Matching-Rules].

The results of the search will be added to the list of input images
in the order of appearance.

Multiple calls to C<input_pattern()> will load
input images in the order they are found.

C<input_pattern()> can be combined with C<input_patterns()>
and C<input_images()>. The input images list will increase
in the order they are called.

B<Caveat>: the regex is parsed, compiled and passed on to L<File::Find::Rule>.
Escaping of special characters (e.g. the backslash) may be required.

B<Caveat>: the order of the matched input images is entirely up
to L<File::Find::Rule>. There may be unexpected results
when filenames contain unicode characters. Consider
these orderings for example:

=over 2

=item * C<blue.png, κίτρινο.png, red.png>,

=item * C<blue.png, γάμμα.png, κίτρινο.png, red.png>,

=item * C<blue.png, κίτρινο.png, γαμμα.png red.png>,

=back

Return value:

=over 4

=item * 0 on failure, 1 on success.

=back

=head2 C<input_patterns($m)>

  $aaFF->input_patterns($m) or die "failed";

Argument C<$m> is an array of arrays each composed of one or two items.
The first argument, which is mandatory, is the search pattern.
The optional second argument is the directory to start the search.
For each item of C<@$m> it calls L<input_pattern($m)>.

C<input_patterns()> can be combined with C<input_pattern()>
and C<input_images()>. The input images list will increase
in the order they are called.

Return value:

=over 4

=item * 0 on failure, 1 on success.

=back

=head2 C<output_filename($m)>

  my $ret = $aaFF->output_filename($m);

It sets or gets the output filename of the animation.

When setting an output filename, make sure you
specify its extension and it does make sense to FFmpeg (e.g. mp4).

Return value:

=over 4

=item * the current output filename.

=back

=head2 C<input_file_with_images($m)>



( run in 1.226 second using v1.01-cache-2.11-cpan-39bf76dae61 )