Issue № 222

Sliced and Diced Sandbags

A note from the editors: This article, a fascinating glimpse into the strange desires of mid-2000s web designers, is now obsolete.

A few weeks ago, while walking through the beautiful Sussex countryside, I decided I wanted to find a way to automate text-wrapping around images with irregular outlines.

Article Continues Below

Seriously, I need to get out more. This article is the result of that ramble through the hills.

The concept#section1

The concept is simple. We want to create a series of “sandbag” divs that we’ll lay over our image; it will be these sandbags and not the actual image that the text will flow around. But we don’t really want to do that ourselves: we want to get PHP to do it for us.

Step 1: Working out the size of the sandbags#section2

We’ll begin by creating an array with the sizes and positions of our “perfect” sandbags. Our “perfect” sandbags would be only 1px high to allow for the smoothest text flow; working these out manually would be absurd, so let’s find a technique that works!

Our sample image, which we want to right-align, currently looks like this:

Our sample image, with left-aligned text.

Looking at our image, I know how I’d size up the sandbags by eye, and that’s exactly what we’re going to do with PHP. We scan along from left to right, if we hit a transparent pixel we think “ooh, let’s go a bit further;” if we hit a solid pixel we say “righto, that’s where that sandbag needs to be.” Then we’ll go down to the next row of pixels and repeat this process until we hit the bottom of the image.

To put it a tad more technically, we want a loop that scrolls through the Y axis, starting at 0 and finishing at the height of the image. For each row, we’ll scroll through the X axis, starting at 0 and ending at the width of the image. Each time we hit a transparent pixel, we’re going to add 1 to the array for that row. When we hit a non-transparent pixel, we’re going to break out of our X loop and go to the next row of pixels. To put it even more technically, that translates to the following code (line wraps marked » —Ed.):

<?php$image  = imagecreatefrompng( 'an_image.png' );
$width  = imagesx( $image );
$height = imagesy( $image );for ( $y=0; $y < $height; $y++ ){
  $imagemap[$y] = 0;
  for ( $x=0; $x < $width; $x++ ){
    $c = imagecolorsforindex( $image, »
imagecolorat( $image, $x, $y ) );
    if ( $c['alpha'] < 127 ){
      break;
    } else {
      $imagemap[$y]++;
    }
  }
}?>

Phew—surely that’s one of the hardest bits out of the way. The imagecolorsforindex function here is great isn’t it? It returns an array of the red, green, blue, and alpha components. If the alpha component of any given pixel is less than 127, then the pixel isn’t fully transparent.

Positioning our bags#section3

The CSS we’re using is pretty minimal. However it turns out that Internet Explorer doesn’t like having a div only 1px high, so we need a little bit of trickery. To keep IE happy with our 1px high sandbags we use the following CSS:

.sandbag-right {
  border: 0; 
  padding: 0;
  font-size: 0;
  margin: 0 0 -1px 0;
  height: 2px;
  float: right; 
  clear: right;
  background: red;
}

By setting the font-size to zero, we can achieve 2px-high divs. If we then throw in a negative bottom margin of 1px before IE really notices what’s going on, we can make them appear to be 1px high. Bingo.

(The red background is just so we can see the sandbags at this stage; we’ll be removing this later.)

Laying down the sandbags#section4

We’ve now got everything in place, so let’s create those sandbags. This requires a simple foreach loop through our $imagemap array. To get the width of our sandbag, we need to remember it’s not the value we’ve already calculated that we want, as that’s actually the size of the empty space next to the sandbag. The actual width of our sandbag is the width of the image minus the value in our array.

We’re going to use the printf function to echo our sandbags into a template. This will make things neater in the long run. (line wraps marked » —Ed.)

<?php$sandbagTemplate = '
';foreach ( $imagemap as $position => $blankPixels ){               
  $sandbagWidth = $width-$blankPixels;
  printf( $sandbagTemplate,$sandbagWidth );
}?>

Step 1: The text-wrap so far.

You can check out the code for step one.

Step 2: Too much of a good thing…#section5

Now we have an array that lets us generate a perfect series of sandbags. After a bit of testing, we see, somewhat annoyingly, that Opera seems to be the only browser in which these “perfect” sandbags work as expected. In most other browsers, we find that our text disobediently overlaps our sandbags.

I tried playing around with various margin sizes, but this just didn’t cut the mustard. Finally, I reluctantly concluded that we needed less-perfect sandbags. Bigger than 1px. But how big? Well, let’s leave that up to the user. I found that 10px to 50px seemed to work quite well, but it may well vary, so we’ll leave it flexible in the function.

Although we’ve found out how to trick IE into allowing us to have those 1px-high sandbags, there’s not really much point if they don’t work, and our function becomes much neater if we enforce a 2px-high minimum. It was upsetting to lose this neat trick, but I took a deep breath and disallowed 1px-high sandbags from the function. (Don’t be sad, this knowledge will come in handy later on.)

Less than perfect#section6

Now we want to loop through that “perfect” array we generated earlier. If we have a sandbag height of 10px, we would want to look at the array in clusters of 10, taking the largest sandbag from each cluster and then outputting that value into a new array. Of course, rather than using a constant number, we’ll just use our variable $sandbagHeight.

The largest sandbag from each section is actually the one with the smallest value in the array (as the array represents the transparent dead space and not the actual sandbag), so we’ll use the handy PHP function min to return the lowest value from the array.

The resulting loop looks like this:

for( $i=0;$i < count($imagemap ); $i = $i+$sandbagHeight) {
  for( $x=0; $x < $sandbagHeight; $x++ ){
    $b = $x + $i;
    if( isset( $imagemap[$b] ) ){
      $section[$b] = $imagemap[$b];
    }
  }
  $sandbag[] = min( $section );
  unset( $section );
}

We also need to set the height of each sandbag. We’ll do that by adding it to our sandbag template (line wraps marked » —Ed.):

$sandbagTemplate = '
';

We’re going to want the possibility of adding some extra padding above the first sandbags and below the last sandbag, and we can easily isolate the first and last item of the array and append an additional style to these as required.

If we now use our $sandbag array instead of our $imagemap array to generate the sandbags we get the following:

Step 2: Sandbags sans our sample image.

You can see how the code is looking in step two.

Step 3: Placing the image behind the sandbag#section7

We’re on the homestretch now. We’ve got the sandbags. All we need to do now is stick the image behind them. What could be simpler?

All we do is… ur… no, wait… we just…

… oh. Oh dear. This was meant to be the easy bit, and looking at it now it turns out it’s quite a big hill.

I’d always just imagined I’d either a) be able to just give the surrounding div a background image or b) actually use an img.

The problem is, the second we give the surrounding div a specific size in order to give it a background image, the text starts to wrap around the div. It overpowers our sandbags. The same goes for putting an img in there. I tried playing around with z-index layers, but every time I thought it was going to work, it fell apart in one browser or another. For the longest time, this had me utterly stumped.

We’ve got our sandbags and they work… it’s just we can’t seem to put anything else in front of our sandbags without destroying their functionality…

Using what we’ve got#section8

… so let’s not put anything else in front of the sandbags.

This is where some radical thinking comes in.

Let’s use the actual sandbags themselves. Why not just give each sandbag the same background image and position that background image relative to each sandbag’s position.

Does it work? Oh yes it does.

Step 3: Sandbags with the sample image in place.

Here’s the code for step three.

Step 4: Adding a pseudo alt attribute#section9

We’ve now got a working function, but we have to accept that we’ve lost a little bit of accessibility in doing this. Fear not, we can add pseudo alt and title attributes to the image. We’ll set these from the function, and put this new variable in an outer div as a title. We’ll also throw it in there as a hidden span before the first sandbag. That way if we turn off the stylesheet we get our alt text, and if we hover over anywhere on the image we get our title attribute.

We’re also going to add a no-repeat to the background, which means we never get the top of the image repeating again in the final sandbag.

Our stylesheet now looks like this:

.sandbagImage span {
  display: none;
}.sandbagRight {
  border: 0; 
  padding: 0;
  font-size: 0;
  margin: 0 0 0 25px;
  float: right; 
  clear: right;
  background: no-repeat;
}</style>

When we create the outer div we’re going to use the following code give us the option of including an alt attribute:

if($alt != ''){
  echo '
‘ . $alt .”; } else { echo ‘

‘; }

(You don’t need to see the code for this, as it’s a minor tweak.)

Step 5: Keeping Safari happy#section10

There’s a fight brewing here between two browser “constraints” that face us here.

In Safari, that “no-repeat” we added doesn’t work where there is negative background positioning, and so if the final sandbag is too big, we get the top of the image repeating in the final sandbag. That’s not good.

So what we can do is calculate the size of each sandbag as we add it to the array. The final time the variable is set will be the size of the final sandbag. We do that by adding a simple “count” to our second loop:

for( $i=0;$i < count( $imagemap ); $i = $i+$sandbagHeight ){
  for( $x=0;$x < $sandbagHeight; $x++ ){
    $b = $x + $i;
    if( isset( $imagemap[$b] ) ){
      $section[$b] = $imagemap[$b];
    }
  }
  $sandbag[] = min( $section );
  $finalSectionSize = count( $section )-1;
  unset( $section );
}

Step 4: Safari-friendly sandbags.

Take a look at the code for step four.

Keeping IE happy#section11

But we still can’t drop that no-repeat. Why not? Well…

Let’s suppose we have an image that is 121px high, split into 10px-high sandbags. Our final sandbag is going to be 1px high.

But as we know, in Internet Explorer we can’t have 1px high divs without some CSS hacking. If we give the final sandbag a negative bottom margin, we’re then going to have to go through a palaver of adding an extra “fake” sandbag to re-establish our bottom margin if required. We don’t want to do this.

That no-repeat still needs to be there because in this situation it could result in the top row of pixels in the image being repeated at the bottom.

Aaaand the code for step five.

Step 6: Allowing for left-alignment#section12

The function as I’ve explained it only allows for right aligning. To left-align, we have to keep a few simple things in mind:

  1. The CSS for our sandbags should float and clear left, not right.
  2. When looping through our initial array we work from right to left, so we start at the width of the image and end at 0, subtracting from x each time instead of adding to it.
  3. The x axis of the background position is always 0px.

I won’t walk through these steps, but…

Our final function#section13

…we can combine everything together into one final glorious function including the option for left-alignment, our last few bits of cross-browser fixing, and some error checking. The final function is simple to call (line wraps marked » —Ed.):

<?php alignedImage( 'an_image.png', 'right', »
'A right aligned blob',30 ); ?>

You can see the code for the final function: step six. Yay!

44 Reader Comments

  1. …just kidding actually 🙂

    Here’s yet another reason why inline styles have not been deprecated.

    Very neat trick. I wonder how this compares to the other technique of slicing the image into little pieces on-the-fly (performance-wise).

  2. …is actually easy to determine: In order to avoid too much div-containers, the height should be at least the font size + line height of the surrounding text. After all, there’s nothing between those lines.

    E.g. a font size of 1em and a line height of 1.5 would make a sandbag’s height of 1.5em.

    Just my two cents

  3. Interesting. I wonder if it’s possible to do the same but use justified text, so it comes up to the actual edges of the sandbags; I think this would look much nicer. (I will have a go but will have to translate your efforts into ASP first)

  4. … but you CAN have 1px high DIV’s with no content in IE. IT doesn’t like the block to be empty – however IE is stupid enough to consider a remark as “something” and obey’s the height… For example: This (probably) wont work in IE:

    but this will…

  5. How long does this code take to process? Just wondering with it being an interpreted language and if you had maybe 10 images like this on a page, reading each file from disc in order to work this out would take time? Or is this pre-processed?

    Nice idea though 🙂

  6. You can shorten the parts:
    div class=”sandbag-right” style=”background: url(an_image.png) -58px -60px no-repeat; width: 192px;

    By adding the parts:
    background-image: url(an_image.png);
    background-repeat: no-repeat;
    height: 30px;
    to the style-definition for the sandbag-right class in the header.

    That will leave the inline style:
    style=”background-position: -58px -60px; width: 192px;”

    And that’s a bit shorter 🙂

  7. Great article, Rob, well done!

    In response to Paul’s comment on server load; it’s a fair point. Pixel processing in GD isn’t the fastest. However, it shouldn’t be hard to add some caching to the script. Instead of using printf, sprintf to compile a string and write the final result to a final, say .cache. Then when you call the alignedImage function, first see if there is a cache file for that image, if so just output the contents of the cache file else process the image. Sure, it might still take time with the initial view of the page, but it’d be a lot faster on subsequent loads.

  8. Kudos that’s a neat trick and would be dead easy to cache. Anyway I met you at Paolo’s wedding I’ll have to chase you for the photos? 😉

  9. Actually, it doesn’t go totally in the dark … The Box Office uses background color approximations, manual adjustment options, etc. Also handy for those who don’t run run PHP on their server or don’t know how to implement it in their current cms/blog.

    wbr,
    B!

  10. this is a really neat trick. although, wouldn’t it be better for loading time if you just used the script once to generate the markup, then copy and paste the markup instead of calculating the sandbags dynamically? i also think i agree with the line-height comment. there’s no reason to split a sandbag in the middle of a line or in between lines. nice job on this!

  11. *Jeremy*: You do have a good point about reducing the load time, although I think Andy Collington’s comment (see above) about using sprintf to cache the string would probably be better than manually copying and pasting the code (although that very same thought crossed my mind originally!).

    *Christoph*: I concur, working out the ‘correct’ sandbag height based on the line-height would be an excellent way of tackling the issue. Thanks for the comment, I’ll definitely utilise it.

    *Dom*: Small world! Drop me a mail and I can send you a link to the best of the wedding photos if Paolo hasn’t already shown you them.

  12. Good stuff! I think I’d tweak it, though, to seperate the alt and title parameters – so I can set one without the other. Mind you, I’d probably only use it for eye-candy images where both properties would be null anyway.

  13. How would I be able to use the line-height value in the php script?

    The only way I see fit is to manually input the line-height in the function call, but that means I would have to go into the code to change that value every time I decide to change the css. Not something for the believers in separation of code and presentation, eh?

    I’m not sure if javascript could pick out the current line height property for an element from the DOM, but even if it’s possible, I still can’t see how to use that value in the (server processed) php code.

    Any ideas?

  14. You know someone had to say this … while this is a neat concept, how is using a tool such as this semantically correct? Isn’t one of the points of ALA that we promote tools which are standards compliant? Also, I would argue that if your image conveys enough meaning that it requires alternative text, then using CSS to set it as a background image and using the title attribute is not sufficient because it will not be displayed if the image is not there, it will only create a tooltip.

    As a reminder, this tool does not take care of IE5-6’s poor handling of transparencies on PNGs. IE5-6 users will still see grayed text like they normally would. Works okay in IE7 (though, I did find that the text gets garbled for text below the medium font setting … and that using the Ctrl+scroolbutton scaled the entire page in the examples).

    Would it be too much to have an optional argument for the script that will let one set a default color to look for so that it doesn’t have to look for transparencies? This would make the script a bit more flexible in that it could work on JPGs for example.

  15. Referencing the question about using “AJAX” above, if you mean dHTML (dont know why you’d have to communicate with the server for something like this) I’ve got a solution that’s mostly done. It uses points outputted for imagemaps (yeah, I found a use for them) to generate the divs at load time inside a specified div or class of divs. I was considering submitting an article to ALA until this article came out. If there’s interest, I’ll put the code up somewhere for others to look at.

  16. First of all i would like to say that it is a great article and it shows things I thought were not possible.

    But where would I use this? And how about all that extra code? Isn’t there a cleaner way to do it?

  17. Thanks for the idea. I have for long been familiar with the “demo”:http://meyerweb.com/eric/css/edge/raggedfloat/demo.html already mentioned in the comments, but this gave me a new idea: A script that slices an image automagically. Sure, it isn’t as convenient for the designer as this approach, but is friendlier for both the server and the page viewer.

    May I present, The “UnBlobifier”:http://www.abo.fi/~hpaul/unblobify.txt .

    ~Please be gentle, this is just a rough sketch of the script and also my first widely published source~

  18. Great idea, but the article itself is let down by having no links to see each stage working, only the raw code. And no final demo link either! At least there is a link in the comments.

    I too echo the plea for the sandbags to fit the height of each text line. But what happens when the user increases the font size in real time?

    Readers may be interested to check out Stu Nicholls’ fantastic approaches to wrapping text around images on his site:

    1. http://www.cssplay.co.uk/menu/flow.html

    2. http://www.cssplay.co.uk/menu/embed.html

  19. It looks to me like this article uses the same technique as Stu Nicholls’ “fantastic approach to wrapping text around images”, except this article takes it a little further by automating the task of calculating element widths.

  20. The second line of text in the final results screenshot looks to be awfully close to the part of the graphic underneath it. As long as we’re calculating horizontal sandbag distances, shouldn’t we be ensuring the same margins vertically too? I assume this is a minor algorithmic addition to the PHP scan of the image.

  21. Hi Rob, just wanted to say this is a nice idea. I’ve avoided doing this kind of thing as it has traditionally involved a rather more static solution. I haven’t tried it yet but when I have a use for it I will certainly give it a go as I appreciate anything that allows a bit of creativity!

    Best of luck with fuelledoncoffee!

    ps Hi to Andy!

  22. Hi Rob,

    Good work on this. It was something I’d noticed was possible about a year ago, and since then worked on a class to do it all based on a png mask file. Works beautifully.

    Actually updated my site (www.leplop.com) back in April to showcase it. Certainly makes for a nice layout effect.

    I like the idea of using ajax to insert the code to improve the markup viewability.

    Anyways, props to you for also figuring it out! The more innovators out there, the better!

  23. On an accessiblity point, you used display:none for you fake alt text, which will be ignored by most screen readers. In terms of unnecessary markup, I agree with Niek Emmen about taking as much of the inline styling out as possible.

    But it is possible to position your image absolutely underneath the sandbags. I did this by putting the text inside the .sandbag-image div, an image tag inside the #example div but outside .sandbag-image, and making the following changes to the CSS:

    #example { width: 530px; position:relative }
    .sandbag-image { position:absolute; z-index:2 }
    #example img { position:absolute; top:20px; right:0; z-index:1 }

    I had to fiddle about with the vertical position of the image.

    This works in Firefox, Opera and IE6. Can’t test on Safari. This makes the alt text fully accessible. (There’s nothing to be done about those non-semantic divs).

  24. Kudos for working out a great and innovative new trick, Rob! I always enjoy it when ALA publishes a fun little “cool hack” article, as they fortunately seem to still be doing, from time to time.

    My main concern with your solution is that neither the image nor its ALT text is visible when CSS is disabled. If I were to use your script, I would update it to also output the image in a regular IMG tag, which would be set to _display: none_. This should allow for much better accessibility, without upsetting the sandbags at all.

  25. Looks like a very useful technique if one is prepared to accept the non-semantic divs.

    An optimisation could be introduced so that adjacent sandbags of the same size are replaced with a single sandbag with the height set appropriately, thus reducing the number of extra divs and therefore the time required for the page to load.

    Tolerances could be used so that adjacent, pre-optimisation, sandbags that are almost the same width are replaced also with a single sandbag that has its width set to the widest of the two.

    Of course deciding on which sandbags to combine could require a fair bit of computation, but if applied along with the earlier suggestion of caching of the results it would be a one-time hit and therefore acceptable.

    As regards setting the sandbag height according to the line-height, surely that would only be possible if font sizes are specified in pixels (generally to be avoided)?

  26. A while ago I used the shim technique on an experimental page (by hand). But instead of using a background image in each sandbag, I used a negative margin on a DIV following them (you could use an image; in my case the image was purely presentational, so there was no need). The page is “here”:http://www.freecog.net/2006/notebook/notebook.html .

    *Henrik/The UnBlobifier*: You probably shouldn’t include the alt text on each slice–someone using a screen-reader will hear it again and again! Just putting it on the first one should be good enough.

  27. Hey,
    I don’t have much more to say about the article, the result and wether or not this is a good ‘method’ for making image-aligned texts.
    But I don’t like the idea in general. I don’t see the benefits of text following the outline of an image. It just makes it harder to focus on the content and it doesn’t contribute to it. Sure it’s nice eye-candy and I can think of some very visual websites (think of an artists portfolio etc.) that would love this kind of thing. But then, very visual websites, usually don’t contain that many lines of text.

    kind regards,
    Mathijs

  28. how about we just nag browser developers to finally give us a standard method to achieve this easily.

    maybe using some css and an svg path and a few new css attributes

    div#puzzle {
    background:url(images/puzzle.jpg)
    clipping-source: url(masks/puzzlepiece.gif) alpha;
    /* will use black/white values of the gif/jpg to define rough outline*/
    clipping-source: url(masks/puzzlepiece.png);
    /* if transparent png is supported will use transparency value */
    clipping-source: url(masks/puzzlepiece.svg);
    /* if svg is supported will use vector path like DTP programs do */
    text-flow: left-clip;
    /* text will flow along shapes left side. If text were to the right it would be flat, if text-flow were ‘surround’ text would flow along all sides like in DTP programs*/
    }

    absurd wishful thinking, I know.

  29. You were all so happy to abandon the table layout “trick”. It validated nicely, but still: Not a correct use of the table-tag. And now you’re all at it again. Articles like this makes A List Apart a bit hypocritical, posing to be pro-standard good-gal/guy.

  30. Espen,

    Whilst I concur that this article isn’t coming from a 100% standards compliant stance, I still think that there’s room for ideas like this to be discussed on ALA without it becoming hypocritical. I’d like to think of it as an ‘interesting’ approach to a problem. I think we can be pro-standard and still have a little ‘fun’ every now and again 😉

  31. Hello Rob.

    First off, thank you for the well-written script. It is an effect I have been wanting to create on my site for a while, and just never got the guts to start writing a script. I had seen this effect done before on Mike Davidson’s website, and so I was aware of a “better” way to create this exact effect.

    So I wrote a new version of your script (a little more sloppy, so you can change it if you want). It seems to work in most browsers, but I couldn’t test in IE (windows) unfortunatly, but I doubt Mike would let anything go wrong in IE.

    You can read more at http://www.mcb.mcgill.ca/~jette/wordpress/2006/09/12/sandbags/

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

Nothing Fails Like Success

Our own @zeldman paints the complicated catch-22 that our free, democratized web has with our money-making capitalist roots. As creators, how do we untangle this web? #LetsFixThis