collage.pl 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/perl
  2. # Daniel "Trizen" Șuteu
  3. # License: GPLv3
  4. # Date: 23 March 2021
  5. # https://github.com/trizen
  6. # Create a collage from a collection of images.
  7. use 5.010;
  8. use strict;
  9. use autodie;
  10. use warnings;
  11. use GD qw();
  12. use POSIX qw(ceil);
  13. use List::Util qw(min);
  14. use File::Find qw(find);
  15. use Getopt::Long qw(GetOptions);
  16. GD::Image->trueColor(1);
  17. my $size = 350;
  18. my $wsize = undef;
  19. my $hsize = undef;
  20. my $wcrop = 1 / 2; # width crop ratio
  21. my $hcrop = 1 / 5; # height crop ratio
  22. my $output_file = 'collage.png';
  23. my $width = undef;
  24. my $height = undef;
  25. sub usage {
  26. my ($code) = @_;
  27. print <<"EOT";
  28. usage: $0 [options] [files / directories]
  29. options:
  30. --size=i : the length of a square tile (default: $size)
  31. --wsize=i : the width of a tile (default: $size)
  32. --hsize=i : the height of a tile (default: $size)
  33. --wcrop=f : width cropping ratio (default: $wcrop)
  34. --hcrop=f : height cropping ratio (default: $hcrop)
  35. --width=i : minimum width of the collage (default: auto)
  36. --height=i : minimum height of the collage (default: auto)
  37. --output=s : output filename (default: $output_file)
  38. example:
  39. $0 --size=100 ~/Pictures
  40. EOT
  41. exit($code);
  42. }
  43. GetOptions(
  44. 'size=i' => \$size,
  45. 'wsize=i' => \$wsize,
  46. 'hsize=i' => \$hsize,
  47. 'wcrop=f' => \$wcrop,
  48. 'hcrop=f' => \$hcrop,
  49. 'width=i' => \$width,
  50. 'height=i' => \$height,
  51. 'output=s' => \$output_file,
  52. 'h|help' => sub { usage(0) },
  53. )
  54. or die("$0: error in command line arguments\n");
  55. sub analyze_image {
  56. my ($file, $images) = @_;
  57. my $img = eval { GD::Image->new($file) } || return;
  58. say "Analyzing: $file";
  59. $img = resize_image($img);
  60. push @$images, $img;
  61. }
  62. sub resize_image {
  63. my ($image) = @_;
  64. # Get image dimensions
  65. my ($width, $height) = $image->getBounds();
  66. # File is already at the wanted resolution
  67. if ($width == $wsize and $height == $hsize) {
  68. return $image;
  69. }
  70. # Get the minimum ratio
  71. my $min_r = min($width / $wsize, $height / $hsize);
  72. my $n_width = sprintf('%.0f', $width / $min_r);
  73. my $n_height = sprintf('%.0f', $height / $min_r);
  74. # Create a new GD image with the new dimensions
  75. my $gd = GD::Image->new($n_width, $n_height);
  76. $gd->copyResampled($image, 0, 0, 0, 0, $n_width, $n_height, $width, $height);
  77. # Create a new GD image with the wanted dimensions
  78. my $cropped = GD::Image->new($wsize, $hsize);
  79. # Crop from left and right
  80. if ($n_width > $wsize) {
  81. my $diff = $n_width - $wsize;
  82. my $left = ceil($diff * $wcrop);
  83. $cropped->copy($gd, 0, 0, $left, 0, $wsize, $hsize);
  84. }
  85. # Crop from top and bottom
  86. elsif ($n_height > $hsize) {
  87. my $diff = $n_height - $hsize;
  88. my $top = int($diff * $hcrop);
  89. $cropped->copy($gd, 0, 0, 0, $top, $wsize, $hsize);
  90. }
  91. # No crop needed
  92. else {
  93. $cropped = $gd;
  94. }
  95. return $cropped;
  96. }
  97. my @photo_dirs = (@ARGV ? @ARGV : usage(2));
  98. $wsize //= $size;
  99. $hsize //= $size;
  100. if ($wsize <= 0 or $hsize <= 0) {
  101. die "$0: size must be greater than zero (got: [$size, $wsize, $hsize])\n";
  102. }
  103. my @images; # stores all the image objects
  104. find {
  105. no_chdir => 1,
  106. wanted => sub {
  107. if (/\.(?:jpe?g|png)\z/i) {
  108. analyze_image($_, \@images);
  109. }
  110. },
  111. } => @photo_dirs;
  112. my $images_len = scalar(@images);
  113. $width //= int(sqrt($images_len)) * $wsize;
  114. $height //= $width;
  115. if ($width % $wsize != 0) {
  116. $width += ($wsize - ($width % $wsize));
  117. }
  118. if ($height % $hsize != 0) {
  119. $height += ($hsize - ($height % $hsize));
  120. }
  121. while (($width / $wsize) * ($height / $hsize) > $images_len) {
  122. $height -= $hsize;
  123. }
  124. while (($width / $wsize) * ($height / $hsize) < $images_len) {
  125. $height += $hsize;
  126. }
  127. my $collage = GD::Image->new($width, $height);
  128. foreach my $y (0 .. $height / $hsize - 1) {
  129. foreach my $x (0 .. $width / $wsize - 1) {
  130. my $source = shift(@images) // last;
  131. $collage->copy($source, $x * $wsize, $y * $hsize, 0, 0, $wsize, $hsize);
  132. }
  133. }
  134. open my $fh, '>:raw', $output_file;
  135. print $fh (
  136. $output_file =~ /\.png\z/i
  137. ? $collage->png(9)
  138. : $collage->jpeg(90)
  139. );
  140. close $fh;