Getting MP3 album artwork using Perl

When I was building an HTML5 and JavaScript-based streaming MP3 player a while back, I quickly discovered that while it was very impressive, the JavaScript and Data URL solution to reading APIC artwork from ID3 images that I'd originally come up with wasn't acceptable performance-wise. It was rather clever, though, since it circumvented the need to actually write any files server-side and offloaded all the heavy work to the client (see this page for details.)

I therefore decided to implement a server-based solution of some kind. A program on the server would respond to CGI requests for the MP3 file by extracting the album artwork to a public directory on the webserver and returning its path to an AJAX request.

I searched in vain on the internet for some time, looking for decent Perl code showing how to get the APIC frame from ID3 tags. I finally discovered a way to retrieve the artwork data from the ID3 header using the ID3::Tag module.

Image postprocessing (i.e. scaling and conversion to JPEG) is a must if you're serving this data over the web, since the artwork data can in some cases be very large indeed. In the iTunes MP3 library I was working with, some of the artwork images were up to 1MB in size. To deal with this, I used the Perl bindings to ImageMagick to convert the artwork data, typically in PNG format, over to JPEG.

The resulting code, suitably modified for general purpose use, is the perl script getArtwork.pl:

Usage: getArtwork.pl [-r [-w width] [-j qual]] [-d dir] file1 file2 ...
Defaults to extracting image to same directory as file

So, for example, the following command would go through all the MP3 files in the directory music, extract the artwork, resize it to a 100x100 image in JPEG format quality 90 and write it to the directory /tmp/ as a file named [nameOfMP3].jpg.

./getArtwork.pl -vr -w 100 -j 90 -d /tmp/ music/*.mp3

You can download it here: getArtwork.pl

Alternately, you can just copy and use the code below:

#!/usr/bin/perl -w

use File::Basename;
use Getopt::Std;
use MP3::Tag;
use Image::Magick;
use strict;
    
# Settings
my $jpeg_quality                    = 90; # Scale is 1-100
my $resize                          = 0;
my $width                           = 300;
my $verbose                         = 0;
my $destdir                         = undef;

my $opt_string = 'hvrj:d:w:e:';
my %opts;

if (!scalar(@ARGV) or $ARGV[0] eq "-h" or $ARGV[0] eq "--help")
{
    usage();
}

getopts( "$opt_string", \%opts ) or usage();
usage() if $opts{h};

if (!scalar(@ARGV))
{
    usage();
}

if(defined($opts{r})) { $resize = 1;                }
if(defined($opts{j})) { $jpeg_quality = $opts{j};   }
if(defined($opts{w})) { $width = $opts{w};          }
if(defined($opts{d})) { $destdir = $opts{d};        }
if(defined($opts{v})) { $verbose = $opts{v};        }

foreach(@ARGV)
{
    my $filepath = $_;
    
    if (! -e $filepath)
    {
        warn("File does not exist: $filepath\n");
        next;
    }
    
    if (-d $filepath)
    {
        warn("Not a file: $filepath\n");
        next;
    }
    
    if ($filepath !~ /\.mp3$/)
    {
        warn("Not an MP3 file: $filepath\n");
        next;
    }

    my $mp3 = MP3::Tag->new($filepath);

    if (!$mp3)
    {
        warn("Couldn't read tags: $filepath\n");
        next;
    }

    $mp3->get_tags();
    
    # Use this to get standard ID3 info
    my ($title, $track, $artist, $album) = $mp3->autoinfo();
    
    # Get base name and suffix
    my($filename, $directories, $suffix) = fileparse($filepath, ".mp3");
    
    if (!exists($mp3->{ID3v2}))
    {
        warn("No ID3v2: $filepath\n");
        next;
    }
    
    # Read APIC frame
    my $id3v2_tagdata   = $mp3->{ID3v2};
    my $info            = $id3v2_tagdata->get_frame("APIC");
    my $imgdata         = $$info{'_Data'};
    my $mimetype        = $$info{'MIME type'};
    
     $mp3->close();
    
    if (!$imgdata) 
    {
        warn("No artwork data found: $filepath\n");
        next;
    }
    
    # If we're not doing anything with the image, we just write it and return
    if (!$resize)
    {    
        # Create destination path w. img mimetype suffix
        my ($m1, $m2) = split(/\//, $mimetype);
        my $dest = $directories . $filename . ".$m2";
        
        # Write image data to file
        open(ARTWORK, ">$dest") or return("Error writing $dest");
        binmode(ARTWORK);
        print ARTWORK $imgdata;
        close(ARTWORK);
        
        print "Wrote '$dest'\n" if $verbose;
        next;       
    }

    # Load data into ImageMagick object
    my ($image, $x, $ret); 
    $image = Image::Magick->new();
    $x = $image->BlobToImage($imgdata);
    warn $x if $x;
    
    # Resize
    if ($resize)
    {
        $x = $image->Scale($width . "x" . $width);
        warn $x if $x;
    }
    
    my $pdir = $directories;
    if ($destdir)
    {
        $pdir = $destdir;
    }
    
    my $dest = $pdir . "/" . $filename . ".jpg";
    
    # Write image to artwork dir
    $x = $image->Write(         magick => 'jpeg', 
                                filename => $dest,
                                quality => $jpeg_quality
                                );
    warn $x if $x;
    print "Wrote '$dest'\n" if $verbose;
}

exit(0);


sub usage
{
    print "Usage: getArtwork.pl [-r resize [-w width] [-j quality]] [-d directory] file1 file2 ...\n";
    print "Defaults to extracting image to same directory as file\n";
    exit(1);
}