favicon_mongler.pl 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. #!/usr/bin/env perl
  2. use v5.36;
  3. use re '/aa';
  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. Overwrites whatever is already at those paths without asking.
  25. Note that inkscape only ever exports PNG -- it has no idea what an .ico is.
  26. Asking it for one, as this script used to, gets you a PNG with the wrong
  27. extension. The real container is assembled with Imager afterwards.
  28. =cut
  29. die "Usage:\n favicon_mongler.pl /path/to/favicon.svg" unless $ARGV[0];
  30. my $icon = Cwd::abs_path( $ARGV[0] );
  31. my $bin = File::Which::which('inkscape');
  32. die "Please install inkscape" if !$bin;
  33. my $dir = File::Basename::dirname($icon) || die "Can't figure out dir from $icon";
  34. # The sizes the site links to directly.
  35. my @png_sizes = qw{32 48 167 180 192 512};
  36. # The sizes that go inside favicon.ico. 16 is not linked anywhere on its own,
  37. # so it only ever exists as a member of the container.
  38. my @ico_sizes = qw{16 32 48};
  39. # Somewhere to put the sizes we need but don't want to leave lying around.
  40. my $scratch = File::Temp->newdir();
  41. # Render each size from the vector source rather than downscaling one big
  42. # raster -- at 16 and 32 pixels the difference is the whole ballgame.
  43. my %rendered;
  44. foreach my $size ( sort { $b <=> $a } uniq( @png_sizes, @ico_sizes ) ) {
  45. my $keep = grep { $_ == $size } @png_sizes;
  46. my $out = $keep ? "$dir/favicon-$size.png" : "$scratch/favicon-$size.png";
  47. print "*** Generating ${size}x${size} .png now... ***\n";
  48. my @cmd = ( $bin, '-w', $size, '-h', $size, $icon, '-e', $out );
  49. # There is no maintained Perl SVG rasterizer to bind instead -- Imager has
  50. # no SVG reader, and Image::LibRSVG was last released in 2006 -- so driving
  51. # the renderer as a subprocess is the only option here.
  52. system(@cmd) and die "Failed to run @cmd: " . ( $? == -1 ? $! : 'exit status ' . ( $? >> 8 ) ); ## no critic (logicLAB::ProhibitShellDispatch)
  53. print "*** Wrote $out ***\n\n";
  54. $rendered{$size} = $out;
  55. }
  56. # An .ico is a container of several images, and browsers do pick the size they
  57. # want out of it, so give them all three rather than one scaled at display time.
  58. print "*** Generating favicon.ico (@{[ join 'x, ', @ico_sizes ]}x) now... ***\n";
  59. my @members = map {
  60. my $img = Imager->new;
  61. $img->read( file => $rendered{$_} ) or die "Could not read $rendered{$_}: " . $img->errstr;
  62. $img;
  63. } @ico_sizes;
  64. Imager->write_multi( { file => "$dir/favicon.ico", type => 'ico' }, @members )
  65. or die "Could not write $dir/favicon.ico: " . Imager->errstr;
  66. print "*** Wrote $dir/favicon.ico ***\n";
  67. 0;