#!/usr/bin/perl -w =pod =head1 NAME imgproc - Process images sent via T-Mobile MMS (and others?) =head1 SYNOPSIS imgproc =head1 DESCRIPTION This program parses an email message sent from a T-Mobile phone with an image attachment, adding a timestamp at the bottom left, and, if the message contains an attachment with type "text/plain", this text is added to the image at the top left. =head1 NOTES =over 4 =item * I wrote a small program to sort cam updates from the rest of my email. You could just create a file, F<~/.forward+cam>, with the following contents: "|/home/yourusername/imgproc.pl" (After making the neccessary adjustments, where "cam" is replaced by a sufficiently random password.) =item * May work with other carriers and/or phones. Hell if I know. =item * The tilde ~ is used as a newline delimiter, because my Motorola RAZR doesn't do newlines easily. The newline character itself is also used as such. =item * Images are resized to 320x240, and all the stuff we do to the image is based on that assumption. =back =head1 TODO Maybe waste some CPU cycles figuring out if Annotate() silently failed. =head1 AUTHOR Johnny Cuervo =head1 LICENSE Released into the public domain 03 Dec 2007. Reiterated 22 Jul 2008. =cut use File::Temp qw(tempfile); use File::Copy; use Mail::Header; use MIME::Base64; use Image::Magick; use DBI; use Digest::MD5; #$font = 'font.ttf'; $pointsize = 14; $lockfile = '/home/cuervo/cam/lock'; sub dolock () { my $waits = 10; while (my $otherpid = readlink $lockfile) { if (! kill 0, $otherpid) { warn "overriding stale lockfile (pid $otherpid)"; last; } die "Waited too long" if (! $waits--); sleep 1; } link $$, $lockfile; } # stamp (img, xPos, yPos, stamptext) # # Annotate the image with visible text. Also do some trickery # (invert the color and increase x/y by 1) to simulate emboss. # sub stamp ($$$$) { my ($img, $x, $y, $stamptext) = @_; $img->Annotate( 'text' => $stamptext, 'geometry' => "+$x+$y", #'font' => $font, 'fill' => 'white', 'pointsize' => $pointsize, ); $x++; $y++; $img->Annotate( 'text' => $stamptext, 'geometry' => "+$x+$y", #'font' => $font, 'fill' => 'black', 'pointsize' => $pointsize, ); } dolock; my ($tmpfh, $tmpfile) = tempfile 'stdinXXXXXX'; open MBOX, ">>", "mbox"; while () { print $tmpfh $_; print MBOX $_; } close MBOX; rename $tmpfile, 'lastmail'; seek $tmpfh, 0, 0; my $hdr = Mail::Header->new($tmpfh); my $bdry = $hdr->get('content-type') || die "Couldn't get content-type header"; die "Couldn't get boundary from $bdry" if ($bdry !~ /boundary="?([^"]+)"?/); $bdry = $1; my ($fh, $filename, $text); $text = ''; # # MIME is a bitch, but I hate the way the modules handle it, and it'd # probably be overkill to use them, anyway. # seek $tmpfh, 0, 0; OUTER: while (<$tmpfh>) { if (/^--$bdry(?:--)?$/) { my $mime_hdr = Mail::Header->new($tmpfh); my $ctype = $mime_hdr->get('content-type'); next if (! defined $ctype); # EOF?! chomp($ctype = (split /;/, $ctype, 2)[0]); if ($ctype eq 'image/jpeg') { IMAGE: while (<$tmpfh>) { last IMAGE if (/^--$bdry(?:--)?$/); push @imgdata, $_; } ($fh, $filename) = tempfile('tmobileXXXXXX'); print $fh decode_base64(join('', @imgdata)); undef @imgdata; redo; } elsif ($ctype eq 'text/plain') { while (<$tmpfh>) { redo OUTER if (/^--$bdry(?:--)?$/); $text .= $_; } } } } die "No image?" if (! defined $fh); close $tmpfh; close $fh; my $image = Image::Magick->new; $image->ReadImage($filename); # Blow away the tempfile. We've got it in memory now. unlink $filename; # Normalize $image->Resize('geometry' => '320x240'); # If there's extra text/plain crap attached, # use it to annotate the image -- no, wait! #stamp $image, 1, 14, $text if (defined $text); # Add the timestamp regardless -- no, wait! #stamp $image, 1, 238, scalar(localtime); # Watermarking. You'll probably want to blow this away if you're # using this. my $watermark = Image::Magick->new; $watermark->ReadImage('/home/cuervo/public_html/img/skull-icon.png'); $watermark->Resize('Geometry' => '12x12'); $image->Composite( 'image' => $watermark, #'Geometry' => '16x12+2+225', 'Geometry' => '+1+1', ); undef $watermark; # Finished # # Screw the timestamp, just use the text. # if (defined($text) && $text ne '') { @_ = split /[\~\n]/, $text, 2; # Two lines? if (scalar(@_) == 2 && $_[1] ne '') { $image->Draw('stroke' => 'white', 'primitive' => 'rectangle', 'fill' => 'gray', 'transparency' => '70', 'points' => '1, 1, 318, 14'); stamp $image, 1, 12, shift @_; } $image->Draw('stroke' => 'white', 'primitive' => 'rectangle', 'fill' => 'gray', 'transparency' => '70', 'points' => '1, 224, 318, 238', ); stamp $image, 1, 236, shift @_; } #$image->Stegano('offset' => 666); # # Fun part. Create an MD5 of the image, in case it's resent after # it was already successful by the carrier/phone. (Has happened.) # Use the MD5 sum to test if we've already seen this exact image # (and exact text, == resend), then use the MySQL failure to insert # a duplicate record as an indicator of such. # $md5 = Digest::MD5->new; $md5->add($image->GetPixels); $dbh = DBI->connect('dbi:mysql:database=cuervo') || die $DBI::errstr; $dbh->do("INSERT INTO cam (md5) VALUES (" . $dbh->quote($md5->b64digest) . ")") || die "cam insert failed"; # Update the tagboard $dbh->do("INSERT INTO wp_liveshoutbox (name, text, url, time)" ." VALUES ('', '*** Cam updated'," ." '/~cuervo/blog/cam', UNIX_TIMESTAMP())"); # New, unique image! copy $filename, sprintf("/home/cuervo/cam/archive/rcvd-%lu.png", time()); # # 99 images of cuervo on the wall, # 99 images of cuervo! # take one down, # pass it around, # 98 images of cuervo on the wall. # $i = 99; while ($i) { my $fmt = '/home/cuervo/public_html/cam/cam-%d%s.png'; my $imgf = sprintf $fmt, $i, ''; my $tnf = sprintf $fmt, $i, '-tn'; if (-f $imgf) { rename $imgf, sprintf($fmt, $i + 1, ''); rename $tnf, sprintf($fmt, $i + 1, '-tn'); } $i--; } my ($cam, $thumb); # Write it back as a png $image->Write('png:' . ($cam = '/home/cuervo/public_html/cam/cam-1.png')); # Write a thumbnail $image->Resize('geometry' => '80x60'); $image->Write('png:' . ($thumb = '/home/cuervo/public_html/cam/cam-1-tn.png')); chmod 0644, $cam, $thumb; exit 0; END { unlink $lockfile; }