Catalyst Model #8: Titles in real typefaces on demand with Imager

Published · Wednesday, 22 July 2009 (Updated · 7 March 2010)

First things first. Font doesn’t mean what you think it means. They are typefaces or faces. There, the pedantic snobbery is off my chest. I hope we can still just be friends.

Let’s do a quick recap of what we’ve covered so far.

Something those have in common: they are not particularly data oriented excepting the log file query engine. The rest are concerned with data but anyone seeing them from the outside of development would probably think of them as services, not data models. I think it would benefit developers to start thinking about models that way. Services encourage you to think through your API deeply and make it open for reuse outside your web application. The idea of services promotes thin controllers. It promotes creativity and there is no better language for exploration of creativity than Perl.

Today we are doing a model service to write text into images. One of the most annoying things about HTML is that it is barely rendered predictably and in the case of fonts it can not even come close because it’s dependent on the client’s machine. Plus most users have the trash heap of faces that come with Windows so you can’t even hope to use nice faces and have it work. If you have a brand to support, you’re in even worst straits. You probably have a face that cost you a hundred if not several thousand dollars and no way to use it in web pages without making it flat in PDFs or dynamic but hostile to SEO in Flash.

Here is the way! Well, limited use anyway. Meant for headers mostly. It makes no sense to render the real content as images though some would have you believe otherwise. It’s also messing with your SEO to even do the titles, so be aware and make sure your alt attributes are set and your images are backgrounds of heading level tags.

./script/myapp_create.pl model Title

Requirements

Imager and the FreeType2 lib underneath it. Installing FreeType2 is up to you if you don’t have it already.

cpan Imager

Make’n’model, I mean service

emacs lib/MyApp/Model/Title.pm
package MyApp::Model::Title;
use strict;
use warnings;
use parent 'Catalyst::Model';
use Imager ();
use Digest::MD5 qw(md5_hex);
use File::Path qw( make_path );
use Path::Class;
use Encode;

__PACKAGE__->config( heading_size => 24 );

sub heading {
    my $self = shift;
    my $text = shift;
    my $refresh = shift;
    my $dir = Path::Class::Dir->new( md5_hex($text) =~ /(\w{8})/g );
    ( my $img_name = $text ) =~ s/\W+/_/g;

    $self->{title_face} or die "Set a TrueType(tm) title_face for this model";
    my $ttf = Path::Class::File->new($self->{title_face});
    -r $ttf or die "$ttf is not readable";

    $self->{file_root} or die "Set a file_root visible to your web paths";
    my $file_root = Path::Class::Dir->new($self->{file_root});
    -w $file_root or die "$file_root is not writeable by application";

    $self->{web_dir} or die "Set a web_dir inside and relative to your file_root";
    my $web_dir = Path::Class::Dir->new($self->{web_dir});

    my $file_path = Path::Class::File->new($file_root,$dir,$img_name.".png");
    my $web_path = Path::Class::File->new($web_dir,$dir,$img_name.".png");
    return $web_path if -e $file_path and not $refresh;

    my $face = Imager::Font->new(file => $ttf,
                                 type => 'ft2',
                                 index => 0,
                                 size => $self->{heading_size});

    my $bbox = $face->bounding_box(string => Encode::decode_utf8($text));

    my $img = Imager->new(xsize => $bbox->total_width,
                          ysize => $bbox->text_height,
                          color => 'white');

    $img->box(filled => 1, color => 'white');

    $img->string(font => $face,
                 text => Encode::decode_utf8($text),
                 x => 0,
                 y => 0,
                 size => $self->{heading_size},
                 color => "black",
                 aa => 1,
                 align => 0 );

    make_path($file_path->dir,
              {
               verbose => 0,
               mode => 0755,
              })
        unless -e $file_path->dir;

    $img->write(file => $file_path->stringify)
        or die "Could not write image ", $img->errstr;

    return $web_path;
}

1;

Controller

./script/myapp_create.pl controller Title
emacs lib/MyApp/Controller/Title.pm
package MyApp::Controller::Title;
use strict;
use warnings;
use parent 'Catalyst::Controller';

sub index :Path Args(0) {
    my ( $self, $c, $text ) = @_;
    $text ||= $c->request->param("title") || "catalyst";
    my $src = $c->model("Title")->heading($text);
    $c->stash( template => "title/index.tt",
               img => { title => $text,
                        src => $src } );
}

sub with_arg :Path Args(1) {
    my ( $self, $c, $text ) = @_;
    $c->detach("index", [ $text ]);
}

sub src :Local Args(1) {
    my ( $self, $c, $text ) = @_;
    return $c->model("Title")->heading($text);
}

1;

Template to match controller

<h1>Rendered typeface (oh, all right! font) titles</h1>

<div style="border:1px solid #aaa; padding: 5px; margin: 5px 0">
<h2>Plain image</h2>
 <img style="border:0" src="[%-img.src %]" alt="[%-img.title | html %]" />
</div>

<div style="border:1px solid #aaa; padding: 5px; margin: 5px 0">
<h2>&lt;h2&gt; with text -indented off page and image background</h2>
<h2 style="text-indent:-900px; margin:0; padding:0; height: 40px;
           background: transparent url([%-img.src %]) no-repeat top left">
 catalyst
</h2>
</div>

<div style="border:1px solid #aaa; padding: 5px; margin: 5px 0;">

<h2>Inlined calls to the model class</h2>

[%-FOR t IN ['PangyreSoft', 'Ashley Rulz!' ] %]
  <div style="text-align:center; margin: 5px;">
    <img style="border:0"
      src="[%-c.model("Title").heading(t) %]" alt="[% t | html %]" />

<p>
<code><span style="white-space:nowrap">&lt;img </span>
<span style="white-space:nowrap">alt="[% "[%" %] "[% t %]" | html [% "%]" %]"</span>
<span style="white-space:nowrap">src="[% "[%" %] c.model("Title").heading("[% t %]") [% "%]" %]"</span>
 /&gt;</span></code>

<br />
<b><i>&hellip;results in&hellip;</i></b>
<br />

<code>[%-FILTER html %]
    <img  alt="[% t | html %]" src="[%-c.model("Title").heading(t) %]" />
[%-END %]</code>
  </div>
[%-END %]

</div>

Configuration, add to myapp.yml

Model::Title:
  # This must be a real TrueType™ or OpenType face.
  title_face: __path_to(etc/Cuprum.otf)__
  file_root: __path_to(root/static/img/title)__
  web_dir: /img/title
  heading_size: 40

You'll need a typeface to run this. I am pretty sure—please correct me if I’m wrong!—the font used in the example, Cuprum, has an open source style license so you can download it here: Cuprum.otf.

Note we configure a larger heading size than is set as the default in the model: 40 v 24.

The file path stuff is tricky because we are configuring two different root paths. One real one for the files to be written and one for the web path to find relative to where MyApp is serving image files. We can verify the real file path for writing files. It’s not trivial to validate the web paths—we’d write a test to do it—so we’re skipping it here.

Create a directory for the generated images. We want one set aside from regular files so we can exclude it from revision control and reset it by erasing it any time. We make the web dir relative to our static file root which is root/static (set up that way in the introduction with this–

  file_root: __path_to(root/static/img/title)__

Requests to /img/title/ will resolve to the real path we’re writing.

mkdir root/static/img/title

That directory must be fully writable by the user running the application so it can create new directories and the image files.

This approach is already fairly efficient because it’s caching the images to disk. Perhaps permanently. If you really wanted to do something like this in a high traffic site though you’d need to be sure that either the paths were written to the templates too or that they were cached in the application so the model wouldn’t be asked to generate it again.

A problem with the current implementation is the filename transformation is naïve and will cause collisions in differing strings. “Code?,” “Code!,” and “Code…” for example will all end up: “code_.png.” But it’s a straightforward, human readable compromise. Something more bomb-proof should be done for a production version.

I recommend playing around with this one. It’s nearly tactile and a lot of fun. You can tweak the API. Add colors, sizes, and more. Imager can do all kinds of transforms and effects as well. Try adding a feature via config and making the API your own.

Screen shot of some titles in action

Titles with Imager: 10 in 10

Go crazy–

./script/myapp_server.pl -d -r -p 3000

Tomorrow, the Lords of Sleep willing, we do TheSchwartz. It’s not written yet though so don’t count on it publishing early.



digg stumbleupon del.icio.us reddit Fark Technorati Faves

« Catalyst Model #7: Page view counter/tracker · Catalyst Model #9: TheSchwartz »
« 10 Catalyst models in 10 days1 »