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.
Seriously, I need to get out more. This article is the result of that ramble through the hills.
The concept#section2
The concept is simple. We want to create a series of “sandbag” div
s 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#section3
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#section4
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 div
s. 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#section5
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…#section6
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#section7
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#section8
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#section9
… 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#section10
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 '
(You don’t need to see the code for this, as it’s a minor tweak.)
Step 5: Keeping Safari happy#section11
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#section12
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 div
s 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#section13
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:
- The CSS for our sandbags should float and clear left, not right.
- 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.
- The x axis of the background position is always 0px.
I won’t walk through these steps, but…
Our final function#section14
…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!
…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).
…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
Is it possible to show the code as it’s delivered to the browser (especially the final version)?
The problem you mention with Safari is _mostly_ fixed with the latest Webkit, though there’s “other problems inherent”:http://bugzilla.opendarwin.org/show_bug.cgi?id=3998 . I don’t know when Apple plans on releasing Safari with this latest Webkit patch.
*John:* You can view the pages as delivered to the browser from here:
“www.fuelledoncoffee.com/tuition/sandbags/”:http://www.fuelledoncoffee.com/tuition/sandbags/
(they’re the pages I used to generate the screenshots)
How about using ajax to do your sandbagging, keeping the original markup free from all those divs and style attributes?
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)
… 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…
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 🙂
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 🙂
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.
Crud, there goes my tool “The Box Office”:http://www.theboxoffice.be/ (since May 2005) totally in the dark 🙁
It does the same actually 😉
Kindly,
B!
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? 😉
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!
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!
*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.
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.
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?
Anyone remember “this”:http://meyerweb.com/eric/css/edge/raggedfloat/demo.html ?
Well I know it’s not quite the same thing, but it _is_ from 4 and a half years ago.
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.
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.
I’m right with Brian on the semantics concerns.
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?
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~
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
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.
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.
I will use a mix between the solution in this article and the old solution in curvelicious.
Thanks for the inspiration!
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!
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!
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).
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.
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)?
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.
Always happy to read ALA’s articles, I’ve found a bad link on step four, it redirects me to a 404 page:
/d/sandbags/step4.php.txt
Cheers,
Giuseppe
Wouldn’t using JavaScript instead favor a more semantic mark-up?
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
Giuseppe:
There _is no link_ on example four.
The text explains why:
bq. You don’t need to see the code for this, as it’s a minor tweak.
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.
Since absolute positioning takes the block out of the stack, couldn’t the image be positioned absolutely over the sandbags?
Never mind, missed the post about this. My bad.
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.
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 😉
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/