img-autocrop.pl 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/perl
  2. # Author: Daniel "Trizen" Șuteu
  3. # License: GPLv3
  4. # Date: 14 June 2015
  5. # https://github.com/trizen
  6. # A generic image auto-cropper which adapt itself to any background color.
  7. use 5.010;
  8. use strict;
  9. use warnings;
  10. use GD qw();
  11. use Getopt::Long qw(GetOptions);
  12. use File::Basename qw(basename);
  13. use File::Spec::Functions qw(catfile);
  14. # Set true color
  15. GD::Image->trueColor(1);
  16. # Autoflush mode
  17. local $| = 1;
  18. my $tolerance = 5;
  19. my $invisible = 0;
  20. my $jpeg_quality = 95;
  21. my $png_compression = 7;
  22. my $directory = 'Cropped images';
  23. sub help {
  24. my ($code) = @_;
  25. print <<"EOT";
  26. usage: $0 [options] [images]
  27. options:
  28. -t --tolerance=i : tolerance value for the background color
  29. default: $tolerance
  30. -i --invisible! : make the background transparent after cropping
  31. default: ${$invisible ? \'true' : \'false'}
  32. -p --png-compress=i : the compression level for PNG images
  33. default: $png_compression
  34. -j --jpeg-quality=i : the quality value for JPEG images
  35. default: $jpeg_quality
  36. -d --directory=s : directory where to create the cropped images
  37. default: "$directory"
  38. example:
  39. perl $0 -t 10 *.png
  40. EOT
  41. exit($code // 0);
  42. }
  43. GetOptions(
  44. 'd|directory=s' => \$directory,
  45. 'i|invisible!' => \$invisible,
  46. 't|tolerance=i' => \$tolerance,
  47. 'p|png-compression=i' => \$png_compression,
  48. 'j|jpeg-quality=i' => \$jpeg_quality,
  49. 'h|help' => sub { help(0) },
  50. )
  51. or die("$0: error in command line arguments!\n");
  52. {
  53. my %cache;
  54. sub is_background {
  55. my ($img, $index, $bg_rgb) = @_;
  56. my $rgb = ($cache{$index} //= [$img->rgb($index)]);
  57. abs($rgb->[0] - $bg_rgb->[0]) <= $tolerance
  58. and abs($rgb->[1] - $bg_rgb->[1]) <= $tolerance
  59. and abs($rgb->[2] - $bg_rgb->[2]) <= $tolerance;
  60. }
  61. }
  62. sub check {
  63. my ($img, $bg_rgb, $width, $height) = @_;
  64. my $check = sub {
  65. foreach my $sub (@_) {
  66. is_background($img, $sub->(), $bg_rgb) || return;
  67. }
  68. 1;
  69. };
  70. my $w_lt_h = $width < $height;
  71. my $min = $w_lt_h ? $width : $height;
  72. my %seen;
  73. # Spiral-in to smaller gaps
  74. # -- this algorithm needs to be improved --
  75. for (my $i = int(sqrt($min)) ; $i >= 1 ; $i--) {
  76. foreach my $j (1 .. $min) {
  77. next if $j % $i;
  78. next if $seen{$j}++;
  79. if (
  80. not $check->(
  81. sub { $img->getPixel($j, 0) },
  82. sub { $img->getPixel(0, $j) },
  83. sub { $img->getPixel($j, $height) },
  84. sub { $img->getPixel($width, $j) },
  85. )
  86. ) {
  87. return;
  88. }
  89. }
  90. }
  91. if ($w_lt_h) {
  92. foreach my $y ($width + 1 .. $height) {
  93. if (not $check->(sub { $img->getPixel(0, $y) }, sub { $img->getPixel($width, $y) })) {
  94. return;
  95. }
  96. }
  97. }
  98. else {
  99. foreach my $x ($height + 1 .. $width) {
  100. if (not $check->(sub { $img->getPixel($x, 0) }, sub { $img->getPixel($x, $height) })) {
  101. return;
  102. }
  103. }
  104. }
  105. return 1;
  106. }
  107. sub make_invisible_bg {
  108. my ($img, $transparent, $bg_rgb, $width, $height) = @_;
  109. foreach my $x (0 .. $width) {
  110. foreach my $y (0 .. $height) {
  111. if (is_background($img, $img->getPixel($x, $y), $bg_rgb)) {
  112. $img->setPixel($x, $y, $transparent);
  113. }
  114. }
  115. }
  116. }
  117. sub autocrop {
  118. my @images = @_;
  119. foreach my $file (@images) {
  120. my $img = GD::Image->new($file);
  121. if (not defined $img) {
  122. warn "[!] Can't process image `$file': $!\n";
  123. next;
  124. }
  125. my ($width, $height) = $img->getBounds();
  126. $width -= 1;
  127. $height -= 1;
  128. my $bg_rgb = [$img->rgb($img->getPixel(0, 0))];
  129. print "Checking: $file";
  130. check($img, $bg_rgb, $width, $height) || do {
  131. say " - fail!";
  132. next;
  133. };
  134. say " - ok!";
  135. print "Cropping: $file";
  136. my $top;
  137. my $bottom;
  138. TB: foreach my $y (1 .. $height) {
  139. foreach my $x (1 .. $width) {
  140. if (not defined $top) {
  141. if (not is_background($img, $img->getPixel($x, $y), $bg_rgb)) {
  142. $top = $y - 1;
  143. }
  144. }
  145. if (not defined $bottom) {
  146. if (not is_background($img, $img->getPixel($x, $height - $y), $bg_rgb)) {
  147. $bottom = $height - $y + 1;
  148. }
  149. }
  150. if (defined $top and defined $bottom) {
  151. last TB;
  152. }
  153. }
  154. }
  155. if (not defined $top or not defined $bottom) {
  156. say " - fail!";
  157. next;
  158. }
  159. my $left;
  160. my $right;
  161. LR: foreach my $x (1 .. $width) {
  162. foreach my $y (1 .. $height) {
  163. if (not defined $left) {
  164. if (not is_background($img, $img->getPixel($x, $y), $bg_rgb)) {
  165. $left = $x - 1;
  166. }
  167. }
  168. if (not defined $right) {
  169. if (not is_background($img, $img->getPixel($width - $x, $y), $bg_rgb)) {
  170. $right = $width - $x + 1;
  171. }
  172. }
  173. if (defined $left and defined $right) {
  174. last LR;
  175. }
  176. }
  177. }
  178. if (not defined $left or not defined $right) {
  179. say " - fail!";
  180. next;
  181. }
  182. my $cropped = GD::Image->new($right - $left + 1, $bottom - $top + 1);
  183. my $index;
  184. if ($invisible) {
  185. $index = $cropped->colorAllocateAlpha(int(rand(256)), int(rand(256)), int(rand(256)), 0);
  186. $cropped->filledRectangle(0, 0, $cropped->width, $cropped->height, $index);
  187. $cropped->transparent($index);
  188. }
  189. $cropped->copyResized(
  190. $img,
  191. 0, # destX
  192. 0, # destY
  193. $left, # srcX
  194. $top, # srcY
  195. $right, # destW
  196. $bottom, # destH
  197. $right, # srcW
  198. $bottom, # srcH
  199. );
  200. my $name = catfile($directory, basename($file));
  201. if ($invisible) {
  202. make_invisible_bg($cropped, $index, $bg_rgb, $cropped->width - 1, $cropped->height - 1);
  203. $name =~ s/\.\w+\z/.png/;
  204. }
  205. open my $fh, '>:raw', $name or die "Can't create file `$name': $!";
  206. print $fh (
  207. $name =~ /\.png\z/i ? $cropped->png($png_compression)
  208. : $name =~ /\.gif\z/i ? $cropped->gif
  209. : $cropped->jpeg($jpeg_quality)
  210. );
  211. close $fh;
  212. say " - ok!";
  213. }
  214. }
  215. @ARGV || help(1);
  216. if (not -d $directory) {
  217. mkdir($directory) || die "Can't mkdir `$directory': $!";
  218. }
  219. autocrop(@ARGV);