recompress_images.pl 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/perl
  2. # Author: Trizen
  3. # Date: 13 September 2023
  4. # Edit: 18 September 2023
  5. # https://github.com/trizen
  6. # Recompress a given list of images, using either PNG or JPEG (whichever results in a smaller file size).
  7. # WARNING: the original files are deleted!
  8. # WARNING: the program does LOSSY compression of images!
  9. # If the file is a PNG image:
  10. # 1. we create a JPEG copy
  11. # 2. we recompress the PNG image using `pngquant`
  12. # 3. we recompress the JPEG copy using `jpegoptim`
  13. # 4. then we keep whichever is smaller: the PNG or the JPEG file
  14. # If the file is a JPEG image:
  15. # 1. we create a PNG copy
  16. # 2. we recompress the JPEG image using `jpegoptim`
  17. # 3. we recompress the PNG copy using `pngquant`
  18. # 4. then we keep whichever is smaller: the JPEG or the PNG file
  19. # The following tools are required:
  20. # * jpegoptim -- for recompressing JPEG images
  21. # * pngquant -- for recompressing PNG images
  22. use 5.036;
  23. use GD;
  24. use File::Find qw(find);
  25. use File::Temp qw(mktemp);
  26. use File::Copy qw(copy);
  27. use File::Spec::Functions qw(catfile tmpdir);
  28. use Getopt::Long qw(GetOptions);
  29. GD::Image->trueColor(1);
  30. my $png_only = 0; # true to recompress only PNG images
  31. my $jpeg_only = 0; # true to recompress only JPEG images
  32. my $quality = 85; # default quality value for JPEG (between 0-100)
  33. my $png_compression = 0; # default PNG compression level for GD (between 0-9)
  34. my $use_exiftool = 0; # true to use `exiftool` instead of `File::MimeInfo::Magic`
  35. sub png2jpeg (%args) {
  36. my $orig_file = $args{png_file} // return;
  37. my $jpeg_file = $args{jpeg_file} // return;
  38. my $image = eval { GD::Image->new($orig_file) } // do {
  39. warn "[!] Can't load file <<$orig_file>>. Skipping...\n";
  40. return;
  41. };
  42. my $jpeg_data = $image->jpeg($quality);
  43. open(my $fh, '>:raw', $jpeg_file) or do {
  44. warn "[!] Can't open file <<$jpeg_file>> for writing: $!\n";
  45. return;
  46. };
  47. print {$fh} $jpeg_data;
  48. close $fh;
  49. }
  50. sub jpeg2png (%args) {
  51. my $orig_file = $args{jpeg_file} // return;
  52. my $png_file = $args{png_file} // return;
  53. my $image = eval { GD::Image->new($orig_file) } // do {
  54. warn "[!] Can't load file <<$orig_file>>. Skipping...\n";
  55. return;
  56. };
  57. my $png_data = $image->png($png_compression);
  58. open(my $fh, '>:raw', $png_file) or do {
  59. warn "[!] Can't open file <<$png_file>> for writing: $!\n";
  60. return;
  61. };
  62. print {$fh} $png_data;
  63. close $fh;
  64. }
  65. sub determine_mime_type ($file) {
  66. if ($file =~ /\.jpe?g\z/i) {
  67. return "image/jpeg";
  68. }
  69. if ($file =~ /\.png\z/i) {
  70. return "image/png";
  71. }
  72. if ($use_exiftool) {
  73. my $res = `exiftool \Q$file\E`;
  74. $? == 0 or return;
  75. defined($res) or return;
  76. if ($res =~ m{^MIME\s+Type\s*:\s*(\S+)}mi) {
  77. return $1;
  78. }
  79. return;
  80. }
  81. require File::MimeInfo::Magic;
  82. File::MimeInfo::Magic::magic($file);
  83. }
  84. sub optimize_jpeg ($jpeg_file) {
  85. # Uncomment the following line to use `recomp-jpg` from LittleUtils
  86. # return system('recomp-jpg', '-q', '-t', $quality, $jpeg_file);
  87. system('jpegoptim', '-q', '-s', '--threshold=0.1', '-m', $quality, $jpeg_file);
  88. }
  89. sub optimize_png ($png_file) {
  90. system('pngquant', '--strip', '--ext', '.png', '--skip-if-larger', '--force', $png_file);
  91. }
  92. @ARGV or die <<"USAGE";
  93. usage: perl $0 [options] [dirs | files]
  94. Recompress a given list of images, using either PNG or JPEG (whichever results in a smaller file size).
  95. options:
  96. -q INT : quality level for JPEG (default: $quality)
  97. --jpeg : recompress only JPEG images (default: $jpeg_only)
  98. --png : recompress only PNG images (default: $png_only)
  99. --exiftool : use `exiftool` to determine the MIME type (default: $use_exiftool)
  100. WARNING: the original files are deleted!
  101. WARNING: the program does LOSSY compression of images!
  102. USAGE
  103. GetOptions(
  104. 'q|quality=i' => \$quality,
  105. 'jpeg|jpg!' => \$jpeg_only,
  106. 'png!' => \$png_only,
  107. 'exiftool!' => \$use_exiftool,
  108. )
  109. or die "Error in command-line arguments!";
  110. my %types = (
  111. 'image/png' => {
  112. files => [],
  113. format => 'png',
  114. },
  115. 'image/jpeg' => {
  116. files => [],
  117. format => 'jpg',
  118. },
  119. );
  120. find(
  121. {
  122. no_chdir => 1,
  123. wanted => sub {
  124. (-f $_) || return;
  125. my $type = determine_mime_type($_) // return;
  126. if (exists $types{$type}) {
  127. my $ref = $types{$type};
  128. push @{$ref->{files}}, $_;
  129. }
  130. }
  131. } => @ARGV
  132. );
  133. my $total_savings = 0;
  134. my $temp_png = catfile(tmpdir(), mktemp("tmpfileXXXXX") . '.png');
  135. my $temp_jpg = catfile(tmpdir(), mktemp("tmpfileXXXXX") . '.jpg');
  136. sub recompress_image ($file, $file_format) {
  137. my $conversion_func = \&jpeg2png;
  138. my $temp_file = $temp_jpg;
  139. if ($file_format eq 'png') {
  140. $conversion_func = \&png2jpeg;
  141. $temp_file = $temp_png;
  142. }
  143. copy($file, $temp_file) or do {
  144. warn "[!] Can't copy <<$file>> to <<$temp_file>>: $!\n";
  145. return;
  146. };
  147. $conversion_func->(png_file => $temp_png, jpeg_file => $temp_jpg) or return;
  148. optimize_png($temp_png);
  149. optimize_jpeg($temp_jpg);
  150. my $final_file = $temp_png;
  151. my $file_ext = 'png';
  152. if ((-s $temp_jpg) < (-s $final_file)) {
  153. $final_file = $temp_jpg;
  154. $file_ext = 'jpg';
  155. }
  156. my $final_size = (-s $final_file);
  157. my $curr_size = (-s $file);
  158. $final_size > 0 or return;
  159. if ($final_size < $curr_size) {
  160. my $saved = ($curr_size - $final_size) / 1024;
  161. $total_savings += $saved;
  162. printf(":: Saved: %.2fKB (%.2fMB -> %.2fMB) (%.2f%%) ($file_format -> $file_ext)\n\n",
  163. $saved,
  164. $curr_size / 1024**2,
  165. $final_size / 1024**2,
  166. ($curr_size - $final_size) / $curr_size * 100);
  167. unlink($file) or return;
  168. my $new_file = ($file =~ s/\.(?:png|jpe?g)\z//ir) . '.' . $file_ext;
  169. while (-e $new_file) { # lazy solution
  170. $new_file .= '.' . $file_ext;
  171. }
  172. copy($final_file, $new_file) or do {
  173. warn "[!] Can't copy <<$final_file>> to <<$new_file>>: $!\n";
  174. return;
  175. };
  176. }
  177. else {
  178. printf(":: The image is already very well compressed. Skipping...\n\n");
  179. }
  180. return 1;
  181. }
  182. foreach my $type (keys %types) {
  183. my $ref = $types{$type};
  184. if ($jpeg_only and $ref->{format} eq 'png') {
  185. next;
  186. }
  187. if ($png_only and $ref->{format} eq 'jpg') {
  188. next;
  189. }
  190. foreach my $file (@{$ref->{files}}) {
  191. if ($ref->{format} eq 'png') {
  192. say ":: Processing PNG file: $file";
  193. recompress_image($file, 'png');
  194. }
  195. elsif ($ref->{format} eq 'jpg') {
  196. say ":: Processing JPEG file: $file";
  197. recompress_image($file, 'jpg');
  198. }
  199. else {
  200. say "ERROR: unknown format type for file: $file";
  201. }
  202. }
  203. }
  204. unlink($temp_jpg);
  205. unlink($temp_png);
  206. printf(":: Total savings: %.2fKB\n", $total_savings),