/ domm

I hack Perl for fun and profit.

Follow me on twitter!
Atom Icom ... on Atom!
<<<<<<<<<<
25.12.2014: Making presents with Perl

Each year around Christmas, I produce a mixtape of my favorite music of the year, which I then hand out as a Christmas gift to friends and family. Usually I choose a nice photo I took for the cover, but this year I did something new: I created 41 unique covers by randomly combining 82 fisheye-shots, using lots of Perl.

This is a rather long article, but it covers generating TOC-files for burning gap-less CDs, filtering images using EXIF, lots of image handling and manipulation using Imager, and dealing with the horrors of PDF point/pixel maths to create some nice PDFs.

You can view the end result of all of the code described in this post here, and all the code is here on github.

Mixing the Music

But before we take a look at the covers, I have to prepare the actual music. I enter all of my vinyls into a simple sqlite3 DB, which I also use to produce this list. Using this DB I get a list of records to consider. In the weeks before Christmas I listen to those records again, to pick my favorite songs. Some songs are obvious candidates, sometimes I pick several and only choose the final one when I know which other tracks I'm going to use.

When I finally have my tracklist, I pack my home turntable, schlepp it into my office (where I have another turntable), hook the turntables up to my very crappy mixer, connect my laptop via my ancient USB audio device, plug in a few more audio devices (CD/MP3 player, because not all music is available on vinyl...) and do the live mix. I use audacity to record the mix and do some minor editing (sometimes I have to retake a botched crossfade, and it's much easier to just fix the fade and paste the new audio than to redo the whole 80 minutes mix). I insert track marks at the right places, and use "export multiple" to generate MP3 of the mix. I also export the labels themselves, to generate cue marks for burning the CDs. This is the first time I reach for Perl.

Generate a TOC file for cdrdao

As the whole point of my mixtape is to have 80 minutes of continuous music, without any gaps between the tracks, I cannot just dump the files into brasero etc. cdrdao supports gap-less burning, but it needs a special TOC-file which has a slightly weird format:

// Track 4
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "mix.wav" 09:08:56 02:41:05

// Track 5
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
FILE "mix.wav" 11:49:61 02:52:30

Most of these instructions are boring setting, but I need the name of the audio file to burn (mix.wav), and, if I want to only use a part of the wav, the starting time and the duration as minutes:seconds:frames. But how do I get those time stamps? I use the labels I exported via audacity. Of course audacity uses another format:

0.000000        0.000000
181.490473      181.490473
346.092249      346.092249
548.756611      548.756611

Each line contains the label as seconds.microseconds. So I need to convert those times into the format used by the toc file. Using this script

Most of this script is simple and straight forward, the only really interesting thing is the time2msf method which converts 346.092249 into 05:46:06; and the calculation of the duration of each track. I guess one could golf this down to a one-liner, but I do like to be able to read my code a few years later (e.g. when audacious decides to change its export format..)

Now I can burn the CDs:

cdrdao write --eject --overburn -n 2014.toc

Customize it

Until now, this is all old code to me. I've been using this setup for a few years. But this year I wanted to create a unique cover per mixtape, using only pictures I took using the Lomo Fisheye Micro 4/3 Lens on my Lumix G2.

Finding my fish-eye shots

The first task was to find all the fish-eye pictures. Because I'm a programmer, I of course rather spend 5 hours writing code than 1 hour manually going through the ~4000 pictures I took in 2014.

My first (but stupid) approach was to analyze all images using Imager and look for pictures that are mainly black around the corners and not-so-black in the center. This involved code like this:

my @blacks = $image->getpixel(x=>\@x, y=>\@y);
    my $hits = 0;
    foreach my $color (@blacks) {
        my @color = $color->rgba;
        my $sum=0;
        for(0..2) {
            $sum+=$color[$_];
        }
        $hits++ if $sum/3 < 20;
    }
    if ($hits == @blacks) {
        say "has fish-border ".$f->basename;
        copy($f->stringify, $fishdir->file($f->basename)->stringify);
    }

But I was getting (at the same time) lots of false positives (eg pictures of easter bonfires or fireworks) and negatives (because the Lomo lenses are rather cheap, they tend to leak quite some light, especially when the lens is pointed at a light source).

So I came up with a much easier (and faster!) solution: As I said, the Lomo lenses are rather cheap. So they do not set any EXIF tags. To find all my fisheye pictures all I had to do was to look at the EXIF tags, and choose those that don't have any lens info. Here's the code

I use Path::Class::Iterator to go through all files, skip all non-JPGs, and use Image::ExifTool to read the EXIF tags. If the LensType says "NO-LENS", I've found a candidate!

Now I use Imager to get the height and width of the image. If the image is not square ($x == $y), I use crop to cut out the center of the image and store it into my fisheye folder. If height and width are the same, I was smart enough to switch the aspect ratio on my camera to 1:1 before taking the shots (interestingly, the height of each picture is 2992 pixel if the ratio is 1:1, but only 2672 if it is 3:2).

I still had to manually go through all the selected pictures, because a) quite a lot of them where crap and b) a few weren't shot with the fisheye lens, but with one of the other two Lomo lenses. Still, the manual work was reduced to nearly nothing. And I have written some code. Yay!

Generate random covers with title

I ended up with 83 nice fisheye shots. My next task was to randomly select two images and generate one new image with a ration of 1:2 (which is the standard ratio of a folded CD cover). Here's the code:

I define the $SIZE of the output image and use boring old readdir to get the list of images. Using Imager, I create a $font object, and I also define some text to be printed on each cover (the mixtape title, "der mensch gehört auf eine sofagarnitur", translates to something like "human beings should stay on a couch", where sofagarnitur is a rather old-fashioned word for a big-ish couch that dominates a living room). $transparent_black is an Imager::Color object that
will render as a quite transparent black (it took me quite a while to figure that out, but I blame my utter lack of knowledge regarding the alpha channel)

I use shuffle from List::Util to shuffle the list of images, then I iterate through half the list using an counter. I grab the first element of the list, and then call get_pair to get another element which has a filename that is not too close to the image I just got. The reason for this is that I do not want the combined cover to contain two images of the same motive.

get_pair does this by looking at the filenames, which are basically incrementing numbers. I calculate the absolute difference between the two numbers and if it less than 50 (meaning that there where less than 50 pictures taken between the two candidates (and even though I sometimes take a lot of pictures of the same thing, it seems that I never took more than 50)), I push the current candidate back onto the list (it will end up at the end), and call get_pair again.

As soon as I have two images, I read them into Imager using get_image. get_image also checks if the image is bigger than $SIZE, which can happen if it was taken using an aspect ratio of 1:1. If it is, the image will be resized to the smaller $SIZE.

I now create a new Imager object that is big enough to hold two of my pictures next to each other (xsize => $SIZE * 2). Using paste, I paste the first image to the left side, and the second image to the right side. Then I draw a box onto the top of the image, using the transparent black I defined earlier. This box will serve as a background for the title-text, which is necessary, as some of the images may be white or very light, thus making it impossible to read the title text.

Using $font->align I draw the title-text into the box onto the image. And then I write the new image to the disk.

BTW, the various absolute values for font-size, ymin, y etc were first guessed and than adapted by hand until I had pleasant results. I assume one could also calculate them, but sometimes it is easier to just go with your gut, and not code everything.

Running this script resulted in 41 new images files, named combined_01.jpg to combined_41.jpg, each consisting of two randomly chosen fish eye shots and with the title added to the top of the right image.

Create a PDF of all covers

The next and final step was to create a PDF file of all those images for easy printing. This is very easy using PDF::Create, as you can see here:

I create a new $pdf object, use get_page_size to get the format of a A4 page in landscape orientation (A4L) and create a new page using the format as the new page's MediaBox.

Then I read all the combined-cover images, make a pdf-image out of each cover and stick each image into a new page.

This is all very simple, expect for the xpos/ypos and xscale/yscale values. Because PDF really sucks.

To un-suck it, we need to do some math:

DIN A4 is 21 cm × 29.7 cm. A CD-Cover is 12×12 cm. My folded cover is 24×12cm (when unfolded, as it has to be during printing).

PFD uses "points" as measurement unit. One point is 1/72 of an inch (Please, America, join the rest of the wold in the 20th century and switch to metric!).

To get the size of my cover images in PDF points I need to convert my 12 cm to inches and multiply by 72:

perl -E 'say 12 / 2.54 * 72'
340.157480314961

But the height of my images is 2672 pixel, so I need to dived the points by that again to get the scaling factor:

perl -E 'say 12 / 2.54 * 72 / 2672'
0.127304446225659

I use this value for xscale/yscale.

To correctly position the image in the center of the page, I calculate the size of the page in PDF points: 842×595. PDF measures from the bottom left corner (what!?!), so to get the right offset I need to subtract the size of my image from the total size and divide by two, resulting in 81 and 127.5 for xpos/ypos.

After running this script I have one PDF with 41 pages, one cover per page. Perfect! Off to the press!

But wait a bit...

Liner notes

I always add liner notes with some smart comments about the songs to my mixtapes. The liner notes are best printed directly onto the back side of the cover. If I want to print all the covers in one go (without having to do "manual duplex", as printer produces are calling it when you have to flip the printed pages around to print the back pages (which always ends in various bad prints as no one knows how to orient the paper before shoving it back into the printer)), I need to have my liner notes inserted directly into the PDF, interlaced with the cover pages.

But as the liner notes contain a lot of text in various formats, and as the deadline was rapidly approaching, I decided to keep it simple and produce the liner notes on Libreoffice and export to PDF. Now I had one PDF with liner notes which needed to be interlaces into the covers. I assume that PDF::Create offers a way to do this, but I had no time left, so I went with an old tool I used several times for combining PDFs: combine-pdf.pl (included in PDF::API2).

I only had change my mkpdf.pl script a bit to produce one pdf per cover, which basically only meant to move some code into the loop. Here's the code:

The I used something like this to generate the proper pdf-merge.pl command line:

perl -E 'say "pdf-merge.pl all_in_one.pdf ".join(" ",map { sprintf("cover_%02d.pdf linernotes.pdf", $_) } (1 .. 41))' > run.sh

And I was done!

Yay!

Tags: ~/bin

Comments (via disqus)

07.12.2014: First release of lib::projectroot
30.11.2014: Please review File::LoadLocalLib
26.10.2014: Videos of Austrian Perl Workshop 2014
05.09.2014: Going to Silicon Valley
29.08.2014: Things I learned at YAPC::Europe 2014 in Sofia
10.07.2014: Where will YAPC::Europe 2015 take place?
04.06.2014: Announcing the Austrian Perl Workshop 2014
27.05.2014: Modification of a read-only value attempted
25.05.2014: App::TimeTracker::Command::Trello
>>>>>>>>>>