Skip to content

Instantly share code, notes, and snippets.

@tayl0r
Last active October 31, 2019 16:55
Show Gist options
  • Save tayl0r/bc04916611a2c3af4d63d62c4832720e to your computer and use it in GitHub Desktop.
Save tayl0r/bc04916611a2c3af4d63d62c4832720e to your computer and use it in GitHub Desktop.
resizes images to the nearest multiple of 4
#!/usr/bin/perl
# license: do whatever you want
# author: taylor steil https://github.com/tayl0r
# purpose: resizes images to a multiple of 4.
# will resize them the least amount possible and also keep the aspect ratio as close to the original as possible
# will also pngcrunch the image
# this is useful for using images in ETC2 Crunch compression, which requires image sizes to be a multiple of 4
# requirement: imagemagick (should be able to apt-get install imagemagick)
# requirement: pngcrush (should be able to apt-get install pngcrush)
# tested on windows 10 WSL (windows-linux-subsystem which is some variant of ubuntu)
# usage: perl imageResizerMultipleOf4.pl /some/folder/with/images
use strict;
use File::Find;
use File::Copy;
use File::Path;
use Digest::MD5 qw(md5);
use Cwd 'abs_path';
my $path = $ARGV[0] or die("1st arg: root path is required, will search recursively");
print "path=$path\n";
find(\&FindImages, $path);
sub ValidDirectory {
my $subDir = $_[0];
my $firstChar = substr($subDir, 0, 1);
if ($subDir eq '.' || $subDir eq '..' || $subDir eq '_' || $subDir eq '.DS_Store' || $subDir eq '.BridgeSort' ||
$firstChar eq '.' || $firstChar eq '#' || $firstChar eq '.' || $firstChar eq '_') { return 0; }
return 1;
}
sub FindImages {
my $fileName = $_;
my $fullDir = $File::Find::dir;
my $fullName = $File::Find::name;
#print "$fullName\n";
if ($fullDir eq $fullName) { return; }
if (-d $fullName) { return; }
if (!($fileName =~ m/\.png$/g)) { return; }
my @dirParts = split /\//, $fullName;
foreach my $dirPart (@dirParts) {
if (&ValidDirectory($dirPart) == 0) { return; }
}
my $partialDir = $fullDir;
$partialDir =~ s/\Q$path//;
my $fileNameLocal = "$partialDir/$fileName";
$fileNameLocal =~ s/^\///;
print "$fileNameLocal\n";
my $cmd;
my $output;
# get image size
$cmd = qq|identify -format "%W %H" "$fileName"|;
$output = `$cmd`;
chomp $output;
if ($output =~ /(\d+) (\d+)/) {
my $width = $1;
my $startWidth = $width;
my $height = $2;
my $startHeight = $height;
# get the mod of each number by 4, that tells us if its a multiple of 4 and how close away it is
my $widthMod = $width % 4;
my $heightMod = $height % 4;
if ($widthMod == 0 && $heightMod == 0) {
# we good
next;
}
# first, figure out which mod is lower, that is the one we want to use for both
if ($widthMod == $heightMod) {
# same
$width = &GetNearestMultiple4($width, $widthMod, $widthMod);
$height = &GetNearestMultiple4($height, $heightMod, $heightMod);
} elsif (($widthMod == 1 || $widthMod == 3) && ($heightMod == 1 || $heightMod == 3)) {
# we have a 1 and a 3, that means we should make both go up
$width = &GetNearestMultiple4($width, 3, $widthMod);
$height = &GetNearestMultiple4($height, 3, $heightMod);
} elsif ($widthMod < $heightMod) {
# width mod is lower, use width mod as the control for both
$width = &GetNearestMultiple4($width, $widthMod, $widthMod);
$height = &GetNearestMultiple4($height, $widthMod, $heightMod);
} else {
# height mod is lower, use height mod as the control for both
$width = &GetNearestMultiple4($width, $heightMod, $widthMod);
$height = &GetNearestMultiple4($height, $heightMod, $heightMod);
}
my $realWidthMod = $width - $startWidth;
my $realHeightMod = $height - $startHeight;
print "\t$startWidth -> $width ($realWidthMod), $startHeight -> $height ($realHeightMod)\n";
# now do some checks to make sure our logic is solid
$widthMod = $width % 4;
$heightMod = $height % 4;
# check if both our numbers are multiples of 4
if ($widthMod != 0 || $heightMod != 0) {
die("not multiple of 4");
}
# check if both mods are 1 or 2, we should have no more 3s
if ($widthMod >= 3 || $heightMod >= 3) {
die("mod too high. $widthMod,$heightMod");
}
# check if both numbers changed in the same direction
if ($widthMod != 0 && $heightMod != 0 && ($startWidth - $width) / ($startWidth - $width) != ($startHeight - $height) / ($startHeight - $height)) {
die("didnt go in same direction. $widthMod,$heightMod");
}
# should never have a cumulative sum of -4 or more
if ($realWidthMod != 2 && ($realWidthMod + $realHeightMod) <= -4) {
die("sum mod >= 4. $realWidthMod,$realHeightMod");
}
# never want to be within 8 higher of a power of 2, otherwise we should probably go to the nearest power of 2 instead
# this script does not handle this, just do it manually
foreach my $pow2 (256, 512, 1024, 2048, 4096) {
my $widthPow2 = $width - $pow2;
my $heightPow2 = $height - $pow2;
if (($width >= $height && $widthPow2 > 0 && $widthPow2 <= 8) || ($height >= $width && $heightPow2 > 0 && $heightPow2 <= 8)) {
die("too close to power of 2: $pow2");
}
}
# finally, actually do the resize
my $widthXheight = "$width" . "x" . "$height";
$cmd = qq|convert "$fileName" -geometry $widthXheight\! "$fileName" 2>&1|;
print "\t$cmd\n";
$output = `$cmd`;
if ($?) {
print "ERROR: $output\n";
}
# and pngcrush it
$cmd = qq|pngcrush -ow "$fileName" 2>&1|;
print "\t$cmd\n";
$output = `$cmd`;
if ($?) {
print "ERROR: $output\n";
}
}
}
# size = width or height
# mod & diff are both the modulus by 4, which tels us how far away we are from a multiple of 4
# mod is what controls if we will go up or down
# diff is how many we need to go up or down by
sub GetNearestMultiple4 {
my ($size, $mod, $diff) = @_;
# if we have a 0 mod, we should use our diff instead
if ($mod == 0) {
$mod = $diff;
}
if ($diff == 0) {
# do nothing with a 0 diff
} elsif ($mod == 1) {
# if we're just 1 off, then we want to go down and not up. so 557 becomes 556
$size = $size - $diff;
} elsif ($mod == 2 || $mod == 3) {
# if we're 2 or 3 off, we want to go up. so 902 or 903 becomes 904
$size = $size + (4 - $diff);
}
return $size;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment