favicon_mongler.pl 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env perl
  2. use 5.040;
  3. use re '/aasx';
  4. use Cwd ();
  5. use File::Basename ();
  6. use File::Which ();
  7. use File::Temp ();
  8. use List::Util qw{uniq};
  9. use Imager ();
  10. =head1 SYNOPSIS
  11. Render a favicon SVG out to every size and format a browser might ask for.
  12. =head2 USAGE
  13. bin/favicon_mongler.pl /path/to/favicon.svg
  14. Writes favicon-<size>.png next to the source SVG for each size below, plus a
  15. favicon.ico, which is what browsers guess at when a page doesn't tell them
  16. otherwise.
  17. The PNG sizes are the ones which actually get asked for: 32 and 48 for the tab
  18. and the desktop shortcut, 167 and 180 for iPads and iPhones, and 192 and 512
  19. for the web app manifest. Everything but the .ico is named in either
  20. components/header.tx or Trog::Routes::JSON::webmanifest, so adding a size here
  21. means adding it there too, and vice versa.
  22. =head2 CAVEATS
  23. Needs inkscape on the PATH to do the rendering, and dies if it isn't there.
  24. Works with either era of inkscape. 1.0 reworked the command line and renamed
  25. the export flag from -e to -o, and 1.x rejects -e outright, so the version is
  26. read off the binary rather than assumed -- see _export_flag().
  27. Overwrites whatever is already at those paths without asking.
  28. Note that inkscape only ever exports PNG -- it has no idea what an .ico is.
  29. Asking it for one, as this script used to, gets you a PNG with the wrong
  30. extension. The real container is assembled with Imager afterwards.
  31. =cut
  32. # Inkscape 1.0 reworked the command line: the export flag went from -e to -o,
  33. # and 1.x errors out on -e rather than quietly accepting it. Ask the binary
  34. # which era it belongs to instead of guessing -- this script has already been
  35. # flipped between the two by hand twice (e23b678, then back in 5f0cdca).
  36. sub _export_flag ($bin) {
  37. # Shelling out, on purpose and on the record. system() cannot hand back
  38. # what the child printed, and reaching for a pipe open instead would only
  39. # be picking the spelling ProhibitShellDispatch happens not to look at --
  40. # which is what Troglodyne::ProhibitPipeOpen now exists to stop. $bin is
  41. # quoted because File::Which returns whatever path it found, spaces and all.
  42. my $version = qx{"$bin" --version}; ## no critic (logicLAB::ProhibitShellDispatch)
  43. my ($major) = ( $version // '' ) =~ m/Inkscape\s+(\d+)/;
  44. die "Could not read a version out of `$bin --version`, got: " . ( $version // '(nothing)' ) unless defined $major;
  45. return $major >= 1 ? '-o' : '-e';
  46. }
  47. die "Usage:\n favicon_mongler.pl /path/to/favicon.svg" unless $ARGV[0];
  48. my $icon = Cwd::abs_path( $ARGV[0] );
  49. my $bin = File::Which::which('inkscape');
  50. die "Please install inkscape" if !$bin;
  51. my $dir = File::Basename::dirname($icon) || die "Can't figure out dir from $icon";
  52. my $export = _export_flag($bin);
  53. # The sizes the site links to directly.
  54. my @png_sizes = qw{32 48 167 180 192 512};
  55. # The sizes that go inside favicon.ico. 16 is not linked anywhere on its own,
  56. # so it only ever exists as a member of the container.
  57. my @ico_sizes = qw{16 32 48};
  58. # Somewhere to put the sizes we need but don't want to leave lying around.
  59. my $scratch = File::Temp->newdir();
  60. # Render each size from the vector source rather than downscaling one big
  61. # raster -- at 16 and 32 pixels the difference is the whole ballgame.
  62. my %rendered;
  63. foreach my $size ( sort { $b <=> $a } uniq( @png_sizes, @ico_sizes ) ) {
  64. my $keep = grep { $_ == $size } @png_sizes;
  65. my $out = $keep ? "$dir/favicon-$size.png" : "$scratch/favicon-$size.png";
  66. print "*** Generating ${size}x${size} .png now... ***\n";
  67. my @cmd = ( $bin, '-w', $size, '-h', $size, $icon, $export, $out );
  68. # There is no maintained Perl SVG rasterizer to bind instead -- Imager has
  69. # no SVG reader, and Image::LibRSVG was last released in 2006 -- so driving
  70. # the renderer as a subprocess is the only option here.
  71. system(@cmd) and die "Failed to run @cmd: " . ( $? == -1 ? $! : 'exit status ' . ( $? >> 8 ) ); ## no critic (logicLAB::ProhibitShellDispatch)
  72. print "*** Wrote $out ***\n\n";
  73. $rendered{$size} = $out;
  74. }
  75. # An .ico is a container of several images, and browsers do pick the size they
  76. # want out of it, so give them all three rather than one scaled at display time.
  77. print "*** Generating favicon.ico (@{[ join 'x, ', @ico_sizes ]}x) now... ***\n";
  78. my @members = map {
  79. my $img = Imager->new;
  80. $img->read( file => $rendered{$_} ) or die "Could not read $rendered{$_}: " . $img->errstr;
  81. $img;
  82. } @ico_sizes;
  83. Imager->write_multi( { file => "$dir/favicon.ico", type => 'ico' }, @members )
  84. or die "Could not write $dir/favicon.ico: " . Imager->errstr;
  85. print "*** Wrote $dir/favicon.ico ***\n";
  86. 0;