October 24, 2012

JavaScript: Letterbox, pillowbox or crop an image to any container size

Very often, I've run across the situation of having images of an unspecified size that need to fit into a container of an unspecified size, either letterboxed, or cropped to fill the container. This is easily accomplished by scaling the image, positioning it within the container, and masking the container's contents. With the following methods, it's easy to crop or letterbox/pillowbox an image for your particular use case.

Here's a method that will give you the size and position of your image, based on its relation to the container's size. The method receives the container's dimensions, the image's original size, and a boolean to specify cropping or letterboxing. The method returns an array with the coordinates and dimensions to reposition and resize your image. If you don't know the size of your images beforehand, you can use this method to find out before using the following code.
var ImageUtil = ImageUtil || {};

ImageUtil.getOffsetAndSizeToCrop = function( containerW, containerH, imageW, imageH, cropFill ) {
  var ratioW = containerW / imageW;
  var ratioH = containerH / imageH;
  var shorterRatio = ratioW > ratioH ? ratioH : ratioW;
  var longerRatio = ratioW > ratioH ? ratioW : ratioH;
  var resizedW = cropFill ? Math.ceil(imageW * longerRatio) : Math.ceil(imageW * shorterRatio);
  var resizedH = cropFill ? Math.ceil(imageH * longerRatio) : Math.ceil(imageH * shorterRatio);
  var offsetX = Math.ceil((containerW - resizedW) * 0.5);
  var offsetY = Math.ceil((containerH - resizedH) * 0.5);
  return [offsetX, offsetY, resizedW, resizedH];
};

To wrap up the cropping functionality, you can use the following method to apply the css styles to your image element based on its size, the container's size, and the type of image resizing you'd like. I've included special cases to anchor the image to top or bottom of the container rather than vertically centering it, in case the images' content requires that type of positioning. Note that the method requires a raw html element reference for the image and container - this doesn't require jQuery or any such library.
ImageUtil.CROP = 'CROP';
ImageUtil.CROP_TOP = 'CROP_TOP';
ImageUtil.CROP_BOTTOM = 'CROP_BOTTOM';
ImageUtil.LETTERBOX = 'LETTERBOX';

ImageUtil.cropImage = function( containerEl, containerW, containerH, imageEl, imageW, imageH, cropType ) {
  var cropFill = ( cropType == ImageUtil.CROP || cropType == ImageUtil.CROP_TOP || cropType == ImageUtil.CROP_BOTTOM );
  var offsetAndSize = ImageUtil.getOffsetAndSizeToCrop(containerW, containerH, imageW, imageH, cropFill);

  // set outer container size
  containerEl.style.width = containerW+'px';
  containerEl.style.height = containerH+'px';

  // resize image
  imageEl.width = offsetAndSize[2];
  imageEl.height = offsetAndSize[3];
  imageEl.style.width = offsetAndSize[2]+'px';
  imageEl.style.height = offsetAndSize[3]+'px';

  // position image
  imageEl.style.left = offsetAndSize[0]+'px';
  imageEl.style.top = offsetAndSize[1]+'px';

  // special y-positioning 
  if( cropType == utensils.ImageUtil.CROP_TOP ) {
    imageEl.style.top = '0px';
    imageEl.style.bottom = '';
  } else if( cropType == utensils.ImageUtil.CROP_BOTTOM ) {
    imageEl.style.top = '';
    imageEl.style.bottom = '0px';
  }
};
The only default styles you'd need on your container and image are as follows:
.cropped-image {
  position: absolute;
}
.container {
  position: relative;
  overflow: hidden;
}