App-ImageMagickUtils
view release on metacpan or search on metacpan
lib/App/ImageMagickUtils.pm view on Meta::CPAN
summary => 'Reduce image size, by default via compressing to JPEG quality 40 and downsizing to 1024p',
description => <<'MARKDOWN',
This utility uses <prog:convert> utility to compress an image into JPEG with
default quality of 40 and downsized to 1024p (shortest side to 1024px).
Output filenames are:
ORIGINAL_NAME.q40.jpg
or (if downsizing is done):
ORIGINAL_NAME.1024p-q40.jgp
MARKDOWN
args => {
%argspec0plus_files,
%argspecopt_quality__def40,
downsize_to => {
schema => ['str*', in=>['', '640', '800', '1024', '1536', '2048']],
default => '1024',
description => <<'MARKDOWN',
Downsizing will only be done if the input image's shortest side is indeed larger
then the target downsize.
To disable downsizing, set `--downsize-to` to '' (empty string), or specify on
`--dont-downsize` on the CLI.
MARKDOWN
cmdline_aliases => {
dont_downsize => {summary=>"Alias for --downsize-to ''", is_flag=>1, code=>sub {$_[0]{downsize_to} = ''}},
no_downsize => {summary=>"Alias for --downsize-to ''", is_flag=>1, code=>sub {$_[0]{downsize_to} = ''}},
S => {summary=>"Alias for --downsize-to ''", is_flag=>1, code=>sub {$_[0]{downsize_to} = ''}},
1536 => {summary=>"Shortcut for --downsize-to=1536", is_flag=>1, code=>sub {$_[0]{downsize_to} = '1536'}},
2048 => {summary=>"Shortcut for --downsize-to=2048", is_flag=>1, code=>sub {$_[0]{downsize_to} = '2048'}},
},
},
skip_whatsapp => {
summary => 'Skip WhatsApp images',
'summary.alt.bool.not' => 'Do not skip WhatsApp images',
schema => 'bool*',
default => 1,
description => <<'MARKDOWN',
By default, assuming that WhatsApp already compresses images, when given a
filename that matches a WhatsApp image filename, e.g. `IMG-20220508-WA0001.jpg`
(will be checked using <pm:Regexp::Pattern::Filename::Type::Image::WhatsApp>),
will skip downsizing. The `--no-skip-whatsapp` option will process such
filenames nevertheless.
MARKDOWN
},
skip_downsized => {
summary => 'Skip previously downsized images',
'summary.alt.bool.not' => 'Do not skip previously downsized images',
schema => 'bool*',
default => 1,
description => <<'MARKDOWN',
By default, when given a filename that looks like it's already downsized, e.g.
`foo.1024-q40.jpg` or `foo.q40.jpg`, will skip downsizing. The
`--no-skip-downsized` option will process such filenames nevertheless.
MARKDOWN
},
%argspecs_delete,
},
args_rels => \%args_rels,
features => {
dry_run => 1,
},
examples => [
{
summary => 'The default setting is to downsize to 1024p',
src => 'downsize-image *',
src_plang => 'bash',
test => 0,
'x.doc.show_result' => 0,
},
{
summary => 'Do not downsize, just recompress to JPEG quality 40, delete original files',
src => 'downsize-image --dont-downsize --delete-original *',
src_plang => 'bash',
test => 0,
'x.doc.show_result' => 0,
},
],
};
sub downsize_image {
require File::Which;
require Image::Size;
require IPC::System::Options;
#require Filename::Type::Image;
my %args = @_;
my $convert_path = File::Which::which("convert");
my $downsize_to = $args{downsize_to};
my $skip_whatsapp = $args{skip_whatsapp} // 1;
my $skip_downsized = $args{skip_downsized} // 1;
unless ($args{-dry_run}) {
return [400, "Cannot find convert in path"] unless defined $convert_path;
return [400, "convert path $convert_path is not executable"] unless -x $convert_path;
}
my ($num_files, $num_success) = (0, 0);
my $trash;
FILE:
for my $file (@{$args{files}}) {
log_info "Processing file %s ...", $file;
$num_files++;
unless (-f $file) {
log_error "No such file %s, skipped", $file;
next FILE;
}
#my $res = Filename::Type::Image::check_image_filename(filename => $file);
my ($width, $height, $fmt) = Image::Size::imgsize($file);
unless ($width) {
log_error "Filename '%s' is not image (%s), skipped", $file, $fmt;
next FILE;
}
if ($skip_whatsapp) {
require Regexp::Pattern::Filename::Type::Image::WhatsApp;
if ($file =~ $Regexp::Pattern::Filename::Type::Image::WhatsApp::RE{filename_type_image_whatsapp}{pat}) {
log_info "Filename '%s' looks like a WhatsApp image, skip downsizing due to --skip-whatsapp option is in effect", $file;
next FILE;
}
}
if ($skip_downsized) {
if ($file =~ /\.(?:\d+p?-)?q(?:\d{1,3})\.\w+\z/) {
log_info "Filename '%s' looks like it's already downsized, skip downsizing due to --skip-downsized option is in effect", $file;
next FILE;
}
}
my $q = $args{quality} // 40;
my @convert_args = (
$file,
);
my $downsized;
#say "D:downsize_to=<$downsize_to>, width=<$width>, height=<$height>, q=<$q>";
DOWNSIZE: {
last unless $downsize_to;
my $ratio;
my $shortest_side = $width > $height ? $height : $width;
last unless $shortest_side > $downsize_to;
$downsized++;
push @convert_args, "-resize", "$downsize_to^>";
} # DOWNSIZE
push @convert_args, "-quality", $q;
my $output_file = $file;
my $ext = $downsized ? ".$downsize_to-q$q.jpg" : ".q$q.jpg";
$output_file =~ s/(\.\w{3,4})?\z/($1 eq ".jpg" ? "" : $1) . $ext/e;
push @convert_args, (
$output_file,
);
if ($args{-dry_run}) {
log_info "[DRY-RUN] Running $convert_path with args %s ...", \@convert_args;
$num_success++;
next FILE;
}
log_info "Running $convert_path with args %s ...", \@convert_args;
IPC::System::Options::system(
$convert_path, @convert_args,
);
if ($?) {
my ($exit_code, $signal, $core_dump) = ($? < 0 ? $? : $? >> 8, $? & 127, $? & 128);
log_error "convert for $file failed: exit_code=$exit_code, signal=$signal, core_dump=$core_dump";
} else {
if ($args{trash_original}) {
require File::Trash::FreeDesktop;
$trash //= File::Trash::FreeDesktop->new;
log_info "Trashing original file %s ...", $file;
# will die upon failure, currently we don't trap
$trash->trash($file);
} elsif ($args{delete_original}) {
# currently we ignore the results
log_info "Deleting original file %s ...", $file;
unlink $file;
}
$num_success++;
}
}
$num_success == 0 ? [500, "All files failed"] : [200];
lib/App/ImageMagickUtils.pm view on Meta::CPAN
that contains extra information, much like how HTTP response headers provide additional metadata.
Return value: (any)
=head2 downsize_image
Usage:
downsize_image(%args) -> [$status_code, $reason, $payload, \%result_meta]
Reduce image size, by default via compressing to JPEG quality 40 and downsizing to 1024p.
This utility uses L<convert> utility to compress an image into JPEG with
default quality of 40 and downsized to 1024p (shortest side to 1024px).
Output filenames are:
ORIGINAL_NAME.q40.jpg
or (if downsizing is done):
ORIGINAL_NAME.1024p-q40.jgp
This function is not exported.
This function supports dry-run operation.
Arguments ('*' denotes required arguments):
=over 4
=item * B<delete_original> => I<bool>
Delete (unlink) the original file after downsizing.
See also the C<trash_original> option.
=item * B<downsize_to> => I<str> (default: 1024)
Downsizing will only be done if the input image's shortest side is indeed larger
then the target downsize.
To disable downsizing, set C<--downsize-to> to '' (empty string), or specify on
C<--dont-downsize> on the CLI.
=item * B<files>* => I<array[filename]>
(No description)
=item * B<quality> => I<int> (default: 40)
Quality setting (for JPEGE<sol>PNG), 1 (best compression, worst quality) to 100 (least compression, best quality).
=item * B<skip_downsized> => I<bool> (default: 1)
Skip previously downsized images.
By default, when given a filename that looks like it's already downsized, e.g.
C<foo.1024-q40.jpg> or C<foo.q40.jpg>, will skip downsizing. The
C<--no-skip-downsized> option will process such filenames nevertheless.
=item * B<skip_whatsapp> => I<bool> (default: 1)
Skip WhatsApp images.
By default, assuming that WhatsApp already compresses images, when given a
filename that matches a WhatsApp image filename, e.g. C<IMG-20220508-WA0001.jpg>
(will be checked using L<Regexp::Pattern::Filename::Type::Image::WhatsApp>),
will skip downsizing. The C<--no-skip-whatsapp> option will process such
filenames nevertheless.
=item * B<trash_original> => I<bool>
Trash the original file after downsizing.
This option uses the L<File::Trash::FreeDesktop> module to do the trashing.
Compared to deletion, with this option you can still restore the trashed
original files from the Trash directory.
See also the C<delete_original> option.
=back
Special arguments:
=over 4
=item * B<-dry_run> => I<bool>
Pass -dry_run=E<gt>1 to enable simulation mode.
=back
Returns an enveloped result (an array).
First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.
Return value: (any)
=head1 FAQ
=head2 I got error message "attempt to perform an operation not allowed by the security policy `PDF' @ error/constitute.c/IsCoderAuthorized/426."
See solutions like described in L<https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion>
=head1 HOMEPAGE
Please visit the project's homepage at L<https://metacpan.org/release/App-ImageMagickUtils>.
=head1 SOURCE
Source repository is at L<https://github.com/perlancar/perl-App-ImageMagickUtils>.
( run in 1.571 second using v1.01-cache-2.11-cpan-39bf76dae61 )