Creating a jQuery plugin to wrap text around images

Do you want to wrap text around the edges of an image, not constrained to the square shape? This tutorial show how it is done.

Example | Download plugin

A “long” time ago, Eric Meyer introduced the concept of ragged float. In 2006 Rob Swan gave us Sliced and Diced Sandbags, an automated way of creating ragged floats using a server side script. With the arrival of the canvas element we can to this on the client. (After creating this plugin I found the jQSlickWrap plugin that does the same thing as my plugin).


There  is no way to read the pixel out of an img-element. However, by using the canvas method getImageData we can collect an array of all the pixels. This is an one dimensional array consisting of the red, green, blue and alpha channels . Each pixel is represented by four entries in the array. One for each channel: [r,g,b,aplha, r,g,b,aplha...]. By iterating through the array, we can find where the image is transparent and where we have visible pixels. We can also do this with color, but this is not yet implemented in this plugin.
The plugin won’t work in browsers that don’t support the canvas element.

First the code:

/**************************************************
*	cutOut
*	Author: Tor Brekke Skjøtskift
*	Version: 0.1
*
**************************************************/
(function($) {
	$.fn.cutout = function(settings) {
     	var config = {
			padding:'5px'
			}, canvas = document.createElement("canvas"), padding="";

		if (!canvas.getContext("2d")) return false;

		var ctx = canvas.getContext("2d");

		if (settings) $.extend(config, settings);
		this.each(function() {
			var $this = $(this);
			var imgEl = $this.get(0);
			//Find relevant styles
			var lineHeight = function (lh) {
				if (lh.indexOf('px') !== -1) {
					return parseInt(lh.replace('px',''),10);
				} else {
					var copy=$this.clone();
				        result = 0;
					$(copy).css({
						padding:'0'
					})
					.html('x')
					.insertBefore($this);
			            	result = copy.innerHeight()/10;
			         	copy.remove();
					return parseInt(result,10);
				}
			}($this.parent().css('line-height'));
			var direction;
			if ($this.css('float') == "left") {
				direction = 'left';
				padding='padding-right:'+config.padding;
			} else if ($this.css('float') == "right") {
				direction = 'right';
				padding='padding-left:'+config.padding;
			} else {
				direction = false;
			}
			if (direction) {
				//set height and width of canvas to same as image
				canvas.width = imgEl.width;
				canvas.height = imgEl.height;
				//draw image on canvas
				ctx.drawImage(imgEl, 0, 0);
				//get pixel data
				var oImageData = ctx.getImageData(0, 0,imgEl.width,imgEl.height );
				 //cache data for performance
				var data = oImageData.data;
				var ragged = [], i = 0, el, count = 0;
				//iterate image height in steps of parent line-heigh
				for (var h = 0; h <= oImageData.height; h=h+lineHeight) {
					for (var h1 = h; h1 < h+lineHeight; h1++) {
						if (direction=='left') {
							for (var w = oImageData.width; w>=0; w--) {
								var i = (h1*oImageData.width + w-1)*4 + 3;
								if (data[i] > 250) {
									ragged[count] = w;
									break;
								} // end if
							} // end for
						} else {
							for (var w = 0; w<=oImageData.width; w++) {
								var i = (h1*oImageData.width + w)*4 + 3;
								if (data[i] > 250) {
									ragged[count] = oImageData.width-w;
									break;
								} // end if
							} // end for
						}
					} // end for
					count++;
				} // end for
				//Create the ragged float
				while(count--) {
					el = document.createElement('div');
					el.setAttribute('style', 'float:'+direction+';clear:'+direction+';background:url('+$this.attr('src')+') no-repeat '+direction+' -'+count*lineHeight+'px;z-index:10;height:'+lineHeight+'px;'+padding+';width:'+ragged[count]+'px');
					$this.after(el);
				} // end while
				$this.remove();
			} else {
				if (window.console) {
					console.log('You need to apply float:left or float:right to the cutout image');
				}
			} // endif
		}); // end each
		return this;
	};
})(jQuery);

Lets have a look at the code, step by step.

var config = {
	padding:'5px'
	},canvas = document.createElement("canvas"), padding="";

	if (!canvas.getContext("2d")) return false;

	var ctx = canvas.getContext("2d");

	if (settings) $.extend(config, settings);

First we create the default settings. For this plugin I have only one setting – padding. We need some padding between the edges of the image and the text to create the nessecary air. I also create one canvas element. Then we test if the browser supports the getContext method to find out if the browser supports canvas. We then create the context.

The canvas element is only used to inspect the pixels in the image, so we don’t need more than one.

this.each(function() {
	var $this = $(this);
	var imgEl = $this.get(0);

For performance purposes I cache a reference to $(this) and the html element itself. “get(0)” gets the original DOM element from the jQuery object.

//Find relevant styles
var lineHeight = function (lh) {
	if (lh.indexOf('px') !== -1) {
		return parseInt(lh.replace('px',''),10);
	} else {
		var copy=$this.clone()
		      result = 0;
			$(copy).css({
				padding:'0'
			})
			.html('x')
			.insertBefore($this);

            	result = copy.innerHeight()/10;
         	copy.remove();
		return parseInt(result,10);
	}

}($this.parent().css('line-height'));
var direction;
if ($this.css('float') == "left") {
	direction = 'left';
	padding='padding-right:'+config.padding;
} else if ($this.css('float') == "right") {
	direction = 'right';
	padding='padding-left:'+config.padding;
} else {
	direction = false;
}

Now we inspect both the parent element of the image and the image itself to find the nessecary styles needed for presentation. First we find the line-height of the parent element. (UPDATE: Added test for lineheight other than px). If you have a look at Eric Meyers original ragged float, he sets the height of the div-elements to 15px. This is not a good way to create ragged floats. In order to get the correct flow of the text, the height of the div should be the same as the line-height of the body text. The next thing we do is to check if the image is floated and if it is floated to the left or right. If the image is not floated, there is no reason to create a text wrap.

if (direction) {
	//set height and width of canvas to same as image
	canvas.width = imgEl.width;
	canvas.height = imgEl.height;

	//draw image on canvas
	ctx.drawImage(imgEl, 0, 0);
	//get pixel data
	var oImageData = ctx.getImageData(0, 0,imgEl.width,imgEl.height );

	 //cache data for performance
	var data = oImageData.data;

If we have a float, we proceed to setting the width of the canvas to the width of the img-element. Using the drawImage method, we place the image at the canvas at the top left position. We can now use getImageData method to retrive an array of pixels. This is stored in the data property of the imageDataObject

Important: Due to security limitations, the getImageData method will not work locally or with external images

Lastly we create a variabel to hold the data for performance.

var ragged = [], i = 0, el, count = 0;
//iterate image height in steps of parent line-heigh
for (var h = 0; h <= oImageData.height; h=h+lineHeight) {

First we create the variables. The array “ragged” will hold the width of the edge of the image.

The for loop loops through the y-axis of the image in steps of the parent line-height. We do this because we are only interested in one width value for each div-element in the ragged float.

for (var h1 = h; h1 < h+lineHeight; h1++) {

We create another loop to go through the pixels in between the steps in the outer loop. We need this value to find the correct index in the imageDataObject.

if (direction=='left') {
	for (var w = oImageData.width; w>=0; w--) {
		var i = (h1*oImageData.width + w-1)*4 + 3;
		if (data[i] > 250) {
			ragged[count] = w;
			break;
		} // end if
	} // end for
} else {
	for (var w = 0; w<=oImageData.width; w++) {
		var i = (h1*oImageData.width + w)*4 + 3;
		if (data[i] > 250) {
			ragged[count] = oImageData.width-w;
			break;
		} // end if
	} // end for
}

First we check the direction of the float. If the image is floated left, we need to start examine the pixels from the right and vice versa. The direction of wich we are examining the pixels are decided in the for-loop. Notice the difference.

The next line is a bit tricky to understand (var i = (h1*oImageData.width + w-1)*4 + 3;). This is where we find the index of the channel of the pixel. As I briefly explained earlier the imageDataObject consists of a one dimensional array with all the color channels. One pixel is represented by four entries in the array. We multiply the h1 variabel, which represents the y-axis of the image with the pixel width of the image. Then we add the w-variabel which represents the x-axis we are currently on, to get the number of pixels we have looped through. Then we need to multiply this by four, because each pixel is represented by four entries in the array, one for each channel. The w-variable is subtracted by one when we examine from the right, so we can inspect the current pixel. We then add 3 to get the aplha channel of the pixel we would like to examine. Remeber the index always start at 0, so the index number three is the fourth entry.


We now check the value of the alpha channel. Complete transparency is 255, but I have set this at 250 just to be safe. If the test goes through and we have a pixel with an alpha channel lower than 250 we add the width to our array and break the loop. We have found the edge.

while(count--) {
	el = document.createElement('div');
	el.setAttribute('style', 'float:'+direction+';clear:'+direction+';background:url('+$this.attr('src')+') no-repeat '+direction+' -'+count*lineHeight+'px;z-index:10;height:'+lineHeight+'px;padding-'+direction+':'+config.padding+';width:'+ragged[count]+'px');
	$this.after(el);
} // end while
$this.remove();

Now the time has come to actually create the ragged float. This is done by creating div elements with the height of the parents line-height. The width is retrieved form our array. We also nedd to add a clear-property to push the div’s to a new line. Here we also add padding and the direction variable to insure corerrect rendering.

We then inserts the div elements after our image element and finally removes the image element from the DOM.

And we are done.

About Tor Brekke Skjøtskift

Senior Webdesigner Media Norge Digital
This entry was posted in jQuery. Bookmark the permalink.

2 Responses to Creating a jQuery plugin to wrap text around images

  1. olivier says:

    Hi, thanks for this plugin, but it’s only work on Firefox. On Opéra, safari or Chrome, the images doesn’t appears. An idea ?

    • Safari return ‘normal’ for line-height. I created the following code to find the line-height i pixels:

      var lineHeight = function (lh) {
      	if (lh.indexOf('px') !== -1) {
      		return parseInt(lh.replace('px',''),10);
      	} else {
      		var copy=$this.clone()
      		      result = 0;
      			$(copy).css({
      				padding:'0'
      			})
      			.html('x')
      			.insertBefore($this);
      
                  	result = copy.innerHeight()/10;
               	copy.remove();
      		return parseInt(result,10);
      	}
      
      }($this.parent().css('line-height'));
      

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>