A Way To Wrap Text Around An Image

February 21st, 2008 | | Uncategorized |

By tom

Nicely wrapping type around an image is a standard of print layout, but it’s never been a practical option for the web. Going back to an entry in one of Eric Meyer’s books, I’d put together an earlier item on slicing images of curves to create the arches you see at the top of this page. It’s an incredibly tedious process when you need to make 50 or so of them, and totally impractical for day-to-day production needs.

I’ve been thinking on and off about how to make this happen on the fly, and I think I’ve come up with something. What I arrived at was taking an image with a transparency component, parsing the image in a graphics application to figure out where the transparency is, and building a stack of image slices and ragging the type around them.

Creating 50 or so images on the fly, storing them somewhere and feeding them out to some unknown number of browsers isn’t practical either. I needed a way to re-use the original image without incurring too much of a processor load anywhere. So I tried using the original image as the background-image of a div tag. With a Perl script analyzing the given image and returning data through an Ajax call, a little bit of Javascript and CSS can do the rest.

This demonstration will be dependent on a couple of sub-optimal conditions. One, the image has to be a gif; you could work out the same thing for a png, but you’d have to account for the multiple levels in its alpha channel and make some more decisions than just transparent-or-not. And two, this is going to rely purely on the image in question having an explicit “align=left” or “align=right” attribute. A more advanced version will look into the stylesheets to find the appropriate float attribute and use that instead. I’m also going to be rolling all of the Javascript and CSS into the demonstration file, so that it’s all in one place.

To start with, let’s build a Perl script to find the image, open it up for parsing and start to work on it.

#! /usr/bin/perl
use strict;
print “Content-type: text/plain\n\n”;

use GD;
use CGI;

All this script is going to return is a series of numbers, so text/plain is the way to go. GD is an additional module not part of the standard distribution, but easy enough to find an install on Win32 or *nix. CGI is the standard of parsing out web queries, which is what we’re going to be doing.

my $transparentIndex;
my ($i, $j, $k);
my $debug = 0; # set to 0 for production, 1 for testing

my $query = new CGI();

my $docroot = $ENV{’DOCUMENT_ROOT’};
my $filename = $query->param(’image’);

Since we’re being hygienic and using “strict,” we’ll predeclare our variables. $transparentIndex will be the color assigned as transparent, $query will contain the parameters the Ajax call will send, including the image that the text will rag around. $docroot is the servers document root. If your web site is an entrant in the obfuscated directory tree this might not work (I’m dealing with one of these this week at work, don’t ask). I like to work debugging messages into the script, and have them conditional on the setting of $debug so I can turn them on and off.

open IMG, “$docroot/$filename” || die “can’t open, $!”;
binmode IMG;
my $myImage = GD::Image->newFromGif(\*IMG) || die “problem, $@”;
close IMG;

So we open the image at $docroot — the server’s document root — and the $filename which will be parsed and passed along by a Javascript call. “binmode” is necessary on Win32 systems and harmless on *nix. $myImage is the new GD object, passed along from the opened file.

$transparentIndex = $myImage->transparent();
my ($width, $height) = $myImage->getBounds();

Calling the transparent() function without parameters reads the transparent index. Then we’ll catch the size of the image. Then we’ll slice it into 10px high rows. This can be anything you want, just remember to change it in the Javascript and CSS attributes later on.

my $j=0;

#sets of left and right offsets
my (@lefts, @rights) = ();

#store all the results in %group
my %group;

#each groupindex consists of 10 pixel rows
my $groupindex = 0;

#within that we’ll count to ten
my $rowindex = 0;

What we’re going to do here is take rows of pixels ten at a time, and find out

  • from the left, what is the first non-transparent pixel and how wide is the rest of it; and

  • from the right, what is the last non-transparent pixel, and how wide is what came before;
  • for each group of ten rows, what is the widest value?

#for each row…
while ( $j < $height ) {
my ( $left, $right, $leftflag ) = 0;
for ( $k=0; $k<$width; $k++ ) {
#test to see if it matches the transparent value. set a flag at the first value
#from the left to mark the end of transparent pixels on the left, then start again to find
#the last nontransparent pixel on the right
if ( ( $leftflag == 0) && ($myImage->getPixel($k,$j) == $transparentIndex) ) { $left = $k }
elsif ( ( $leftflag == 0 ) && ($myImage->getPixel($k,$j) != $transparentIndex) ) {
$leftflag = 1;
if ( $debug == 1 ) { print “set a flag at $j $k\n” }
}
elsif ( ( $leftflag == 1 ) && ($myImage->getPixel($k,$j) != $transparentIndex) ) { $right = $k }
}
if ( $left == $width-1 ) { $right = 0 }
if ( $debug == 1 ) {
print “row $j: left $left, right $right\n”;
}

push @lefts, $left;
push @rights, $right;
if ( $rowindex == 9 ) {
@lefts = sort { $a <=> $b } @lefts;
@rights = sort { $a <=> $b } @rights;
$group{$groupindex}{’left’} = $lefts[0];
$group{$groupindex}{’right’} = $rights[$#rights];
$groupindex++;
$rowindex = 0;
( @lefts, @rights ) = () ;
}
else { $rowindex++ }
$j++;
}

If you activate some of the debugging code you can see better what’s happening. We’re making a hash of arrays, keyed from 0 to the height divided by 10. Each hash value consists of two arrays, one for the left values and one for the right. Once we have that all read in, we sort the hash on keys, sort each right and left array numerically to find the highest value, which will serve (based on the total width of the image) as the width of the div we’ll create later.

foreach my $segment ( sort { $a <=> $b } keys %group ) {
if ( $debug == 1 ) {
print “\n\n$segment: $group{$segment}{’left’}, $group{$segment}{’right’}\n”;
print $width - $group{$segment}{’left’} , “\n”;
print $width - $group{$segment}{’right’} , “\n”;
}
if ( $query->param(’align’) eq ‘left’ ) {
print $group{$segment}{’right’}, ‘ ‘;
}
elsif ( $query->param(’align’) eq ‘right’ ) {
print $width - $group{$segment}{’left’}, ‘ ‘;
}
}

These last print lines are what gets returned to the Ajax call, depending on whether align=right or align=left was sent along in the query string. For the example I’ll give, it looks like this:

48 48 59 59 59 59 59 59 59 59 59 59 59 59 34 33 31 76 76 76 76 76 76 76 76 76 48 48 59 62 62 58 41 40 48 52 54 54 54 51

So now we have taken the image, analyzed it an determined for each stripe of 10 pixel high rows what is the widest point. What we can do with that is, create a 10 pixel high row on the web page and crop it at that width. We’ll use Javascript to accomplish this.

You can see the full Javascript source on the example page. But a few salient points: It’s a constant annoyance, but you can’t just go looking for page elements based on calls in a script at the top of the page. The script fires off before the page is even downloaded, let alone built, and will just come back and say “duh, I can’t find any images.” So it’s best to put a trap of some kind in the beginning that times out for a few milliseconds until it finally comes back with something. The preferred way is to look for a specific element based on a known ID. Since I don’t know what ID you’ll be assigning to anything on your page, this just looks for the array of images. Once that’s there, it can start looking through them. I’ve classed the images I want ragged as class=”ragged”.

var stem = images[i].src.match(/(http(s?):\/\/[^/]*)/);
var newimage = images[i].src.replace(/http(s?):\/\/[^/]*\//,”);
var requestURI = stem[0] + ‘/cgi-bin/rag.pl?image=’ + newimage + “&align=” + images[i].align;
sendRequest(requestURI,images[i],handleRequest);
}
}

Here we take the source of the image and separate out the URL stem from the URL path. The path is passed along to the Ajax call, along with the images "align" value, in a query string. The sendRequest uses a stock set of Ajax calls I picked up from Quirksmode.

Once we have our series of numbers back we hide the original image:

image.style.display = ‘none’;

Note that this has to be done before the next steps. If it comes after, IE7 will not hide it, or even remove it.

var newDivWidths = req.responseText.split(” “);
for ( i=0; i var newDiv = document.createElement('div');
newDiv.style.width = newDivWidths[i] + 'px';
newDiv.className = 'ragged' + image.align;
newDiv.style.backgroundImage = 'url(' + image.src + ')';
//uncomment this next line to see where the edges of the new divs are
//newDiv.style.border = "1px solid #f00";
var topPosition = -i * 10; //alert(topPosition + 'px ' + image.align);
newDiv.style.backgroundPosition = image.align + ' ' + topPosition + 'px ';
if ( newDiv.style.width) { image.parentNode.insertBefore(newDiv,image) }
}

After separating the numbers, we step through them one-by-one and create a div element, set its pixel width to the value, assign the right class name, and set the background image as the original image. Then we can set the offset of the background image, moving it 10 pixels for each div so that it lines up seamlessly with the next. Then we insert this new div into the document object.

The last piece is the add some css classes to accommodate these new divs:

div.raggedright { float: right; margin: 0 0 0 15px; clear: right; height: 10px; padding: 0; font-size:1px}
div.raggedleft { float: left; margin: 0 15px 0 0; clear: left; height:10px; padding: 0; font-size:1px }

The font-size is necessary for IE7. Without it, even with the divs empty IE7 arbitrarily makes the divs tall enough to accommodate text, with unfortunate results. I thought they fixed the expanding box problem?

The result, in crude form, is here. The image on the left is one continuous gif some 400 pixels high, but by slicing it into 40 or so pieces and floating them all left, the text flows around them in a shapely way. I’ve only looked at in in IE7 and Firefox 2, let me know if you find anything strange in other browsers.

One Response to ' A Way To Wrap Text Around An Image '

Leave a reply


Subscribe to comments with RSS or TrackBack to ' A Way To Wrap Text Around An Image '.

December 2008
S M T W T F S
« Nov    
 123456
78910111213
14151617181920
21222324252627
28293031  

New on Flickr

DSC_0431
DSC_0400
DSC_0462
DSC_0457
DSC_0451
DSC_0445
DSC_0443
Brady Bend Cemetery

Archives