Skip to content

Posts from the ‘tutorials’ Category

18
May

Revisited: PHP Thumbnails

This is a revisit of my earlier post How To: PHP Thumbnails – Resize images with PHP

After having used the createImage functionality myself for quite some time I decided to revisit it. Make it more “OO” and portable. The class itself worked as advertised and simply made an resized image from an existing image. The reasons to revisit tho are that the code itself isn’t consistent, there is no clear naming convention and the working of it would be better of if all the classes were seperated.

Starting with this concept and looking over the code already created I figured there would be use of the Factory Pattern, an Image class would recieve the file it would work with, and search for possible classes to handle that file. Also, almost all of the parameters were pulled from the constructor and can now be set using general getters & setters. All setters return the current object so it is a fluent interface.

So, let’s take a look at the code, We will have multiple files for this one, let’s start with the Base class

Image.php

<?php
namespace Library;

/**
* Abstract class Image.
* Uses factory pattern to intatntiate child objcets
*/
abstract class Image {

    /**
     * The file
     */
    protected $_file = null;

    /**
     * the image type
     */
    protected $_type = null;

    /**
     * image name
     */
    protected $_name = null;

    /**
     * image extension
     */
    protected $_extension;

    /**
     * image data
     */
    protected $_image = null;

    /**
     * image quality
     */
    protected $_quality = 100;

    /**
     * Image height
     */
    protected $_height = null;

    /**
     * Image width
     */
    protected $_width = null;

    /**
     * Image max height
     */
    protected $_maxHeight = null;

    /**
     * Image max width
     */
    protected $_maxWidth = null;

    /**
     * Image current Height
     */
    protected $_curHeight = null;

    /**
     * Image current Width
     */
    protected $_curWidth = null;

    /**
     * Destination folder
     */
    protected $_destination = null;

    /**
     * Factory Method
     * Accepts a file string and defines the type and instantiates the corresponding
     * child class
     *
     * @param string $file
     */
    public static function factory($file = null) {
    	/* Checks whether a file is given */
        if (null === $file) {
        	throw new \InvalidArgumentException("Image factory requires a file, none given");
        }

        /* check if the file itself exists */
        if (!file_exists($file) && !is_dir($file)) {
        	throw new \Exception("Image file with name '" . $file . "' does not exist or is a directory");
        }

        /* check if file is an image */
		if (!($type = exif_imagetype($file))) {
			throw new \Exception("File '" . $file . "' is not a image");
		}

		/* get the mime type, for a list of possible types: http://php.net/manual/en/function.image-type-to-mime-type.php */
		$mimetype = image_type_to_mime_type($type);

		/* Remove the image/application in the front */
		$type = end(explode('/', $mimetype));

		/* And remove the "-" and "." */
		$type = str_replace("-", "", $type);
                $type = str_replace(".", "", $type);

		/* Capatalize it */
		$type = strtoupper($type);

		/* check if the class file exists */
		if (!file_exists($type . ".php")) {
			throw new \Exception("Class file for '" . $type . "' could not be found.");
		}

		include $type . ".php";
		$classname = __NAMESPACE__ . "\\" . $type;
		return new $classname($file);
    }

    /**
     * Constructor, recieves path to file and filetype
     */
    protected function __construct($file, $type) {
        /* Checks whether a file is given */
        if (null === $file) {
        	throw new \InvalidArgumentException("Image constructor requires a file, none given");
        }

        /* check if the file itself exists */
        if (!file_exists($file) && !is_dir($file)) {
        	throw new \Exception("Image file with name '" . $file . "' does not exist or is a directory");
        }

		if (!exif_imagetype($file)) {
			throw new \Exception("File '" . $file . "' is not a image");
		}

		/**
		 * Settings some basic values here. such as path to file.
		 * Sets current destionation / extension / name / current width and height all based on
		 * the current file
		 */
		$this->_file = $file;
		$fileinfo = pathinfo($file);
		$this->_destination = realpath($fileinfo['dirname']) . DIRECTORY_SEPARATOR;
		$this->_name = $fileinfo['filename'];
		$this->_extension = '.' . $fileinfo['extension'];
    	$this->_type = $type;
    	list($this->_curWidth, $this->_curHeight) = getimagesize($this->_file);
    }

    /**
     * returns the file
     */
    protected function getFile() {
    	return $this->_file;
    }

    /**
     * returns the image
     */
    protected function getImage() {
    	return $this->_image;
    }

    /**
     * Returns the image name
     */
    public function getName() {
    	return $this->_name;
    }

    /**
     * Sets the image name
     */
    public function setName($name) {
    	if (!is_string($name) || empty($name)) {
    		throw new \Exception("Name must be a string and not empty, '" . $name . "' given");
    	}
    	$this->_name = $name;
    	return $this;
    }

    /**
     * gets the extension
     */
    protected function getExtension() {
    	return $this->_extension;
    }

    /**
     * Returns the image quality
     */
    public function getQuality() {
    	return $this->_quality;
    }

    /**
     * Sets the image quality
     */
    public function setQuality($quality) {
		if (!is_numeric($quality)) {
			throw new \Exception("Quality must be number, '".$quality."' given");
		}
		if ($quality < 0 || $quality > 100) {
			throw new \Exception("Quality must be between 0 and 100");
		}
    	$this->_quality = $quality;
    	return $this;
    }

    /**
     * Gets the height
     */
    public function getHeight() {
		return $this->_height;
	}

	/**
	 * Sets the height
	 */
	public function setHeight($height) {
		$this->_height = $height;
	}

	 /**
     * Gets the width
     */
	public function getWidth() {
		return $this->_width;
	}

	/**
	 * Sets the width
	 */
	public function setWidth($width) {
		$this->_width = $width;
	}

	/**
	 * return the image max height
	 */
	public function getMaxHeight() {
		return $this->_maxHeight;
	}

	/**
	 * sets the image max height
	 */
	public function setMaxHeight($maxHeight) {
		if (!is_numeric($maxHeight)) {
			throw new \Exception("Max Height must be number, '".$maxHeight."' given");
		}
		$this->_maxHeight = $maxHeight;
    	return $this;
	}

	/**
	 * Returns the image max width
	 */
	public function getMaxWidth() {
		return $this->_maxWidth;
	}

	/**
	 * sets the image max width
	 */
	public function setMaxWidth($maxWidth) {
		if (!is_numeric($maxWidth)) {
			throw new \Exception("Max Width must be number, '".$maxWidth."' given");
		}
		$this->_maxWidth = $maxWidth;
    	return $this;
	}

    /**
     * Gets the current height
     */
    public function getCurHeight() {
		return $this->_curHeight;
	}

	/**
	 * Sets the current height
	 */
	public function setCurHeight($curHeight) {
		$this->_curHeight = $curHeight;
		return $this;
	}

	 /**
     * Gets the current width
     */
	public function getCurWidth() {
		return $this->_curWidth;
	}

	/**
	 * Sets the current width
	 */
	public function setCurWidth($curWidth) {
		$this->_curWidth = $curWidth;
		return $this;
	}

	public function getDestination() {
		return $this->_destination;
	}

	public function setDestination($destination) {
		if (!is_dir($destination)) {
			throw new \Exception("Destination must be an (existing) folder, '".$destination."' given");
		}
		if (!is_writable($destination)) {
			throw new \Exception("Cannot write to '".$destination."'");
		}
		$this->_destination = rtrim($destination, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    	return $this;
	}

	/**
	 * Creates a black image with the original or specified height/width of the image
	 * @param $width = maximum width of the image
	 * @param $height = maximum height of the image
	 */
	protected function createImage() {

		if ($this->_maxWidth === null && $this->_maxHeight === null) {
			/* No calculation needed, using original sizes */
			$this->_width = $this->_curWidth;
			$this->_height = $this->_curHeight;
			$this->_image = imagecreatetruecolor($this->_width, $this->_height);
			return $this;
		}

		/* Calculations wether this is a landscape or portrait image */
		if (($this->_curWidth / $this->_maxWidth) > ($this->_curHeight / $this->_maxHeight)) {
			/* Landscape */
			if ($this->_curWidth > $this->_maxWidth) {
				$this->_width = intval($this->_maxWidth);
				$this->_height = intval($this->_curHeight * ($this->_maxWidth / $this->_curWidth));
			} else {
				$this->_width = $this->_curWidth;
				$this->_height = $this->_curHeight;
			}
		} else {
			/* Portrait */
			if ($this->_curHeight > $this->_maxHeight) {
				$this->_height = intval($this->_maxHeight);
				$this->_width = intval($this->_curWidth * ($this->_maxHeight / $this->_curHeight));
			} else {
				$this->_width = $this->_curWidth;
				$this->_height = $this->_curHeight;
			}
		}

		/* Create the black image with the new dimension */
		$this->_image = imagecreatetruecolor($this->_width, $this->_height);
		return $this;
	}

	/**
     * Saves the image to disk
     */
    abstract function save();

    /**
     * echos the image
     */
    abstract function show();
}

From top to bottom things to mention are:
Namespace! Change this to your own library. You can remove it. But you have to keep in mind that __NAMESPACE__ is used in the factory method, you will have to refactor that as well

The class is abstract! that means you cannot do:
$image = new Image($filename);
You are required to extend it.

The factory function, it takes a filename. Does some safety checks and defines the image mime-type. Using this mime-type it will look if it can find a class to handle it. (for example: image/jpeg) would need a file and class named “JPEG.php” (note that to support e.g. “vnd.microsoft.icon” the dots and dashes are removed, note: using preg_replace doesn’t become faster then using str_replace before it you need to replace atleast 4 chars source. note2: haven’t tested this myself and the source is very old).

The mime-type is searched for using exef_imagetype which returns a constant value for its type (1 to 17) and image_type_to_mime_type is used to return the mime-type. According to php.net exif_imagetype is significantly faster then getimagesize. I did some testing using XDebug and over 10000 iterations the difference is a about 10%. It took getimagesize() 4.5 seconds (rounded) to find the imagetype upto 10000 times and it took exif_imagetype only 4.1 seconds.

The constructor, it will be called from it’s children with a path to the file, after doing some more safety checks we use php’s pathinfo function to get a lot of information from this file.

in the setDestination function we use a little trick. Someone can use setDestionation in 2 different ways.

<?php
//1
$image->setDestination("folder");
//2
$image->setDestination("folder/");

In order to accomodate both we simply remove all directory separators from the right side of the string and place 1 behind it. This way we know that there is always 1, and only 1 directory separator on the right side of the file.

in the createImage function we do the same as we did in in calcImgSize(), we create a black square (that is what imagecreatetruecolor actually does) based on the maximum height and width we were told

Finnaly we define 2 abstract functions, this means that classes extending this class are required to implement these functions. Namely: save() and show(). We used to only be able to save an image, now with some modification to how the destination of the new file is reached, we will still be able to do this. New tho is the fact that we will be able to just echo the image itself. This is usefull if we were for example to make a thumb.php file which could be used to dynamically load an image in the right size in a webpage. Maybe I will write an example of this later.

Now, let’s look at the child classes. I will look only at the PNG class since it has a little modification to it.

PNG.php

<?php
namespace Library;

class PNG extends Image {

	const MIME_TYPE = "image/png";

	public function __construct($filename) {
		parent::__construct($filename, self::MIME_TYPE);
                /* make sure the correct quality is used */
                $this->setQuality(0);
	}

	/**
	 * Create a true color image based on source
	 */
	protected function create($maxHeight = null, $maxWidth = null) {
		if (null !== $maxHeight) {
			$this->setMaxHeight($maxHeight);
		}
		if (null !== $maxWidth) {
			$this->setMaxWidth($maxWidth);
		}

		$this->createImage();
		$image = imagecreatefrompng($this->getFile());
		imagecopyresampled($this->getImage(), $image, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getCurHeight(), $this->getCurWidth());
		imagedestroy($image);
	}

	/**
	 * Shows the image
	 */
	public function show($maxHeight = null, $maxWidth = null) {
		$this->create($maxHeight, $maxWidth);
		header("Content-type: " . self::MIME_TYPE);
		imagepng($this->getImage());
	}

	/**
	 * Saves the image
	 */
	public function save($maxHeight = null, $maxWidth = null) {
		$this->create($maxHeight, $maxWidth);
		if (imagepng($this->getImage(), $this->getDestination() . $this->getName() . $this->getExtension(), $this->getQuality())) {
			return $this;
		}
	}

	/**
	 * Override parent setQuality, Quality works different for PNGs
	 */
    public function setQuality($quality) {
		if (!is_numeric($quality)) {
			throw new \Exception("Quality must be number, '".$quality."' given");
		}
		if ($quality < 0 || $quality > 100) {
			throw new \Exception("Quality must be between 0 and 100");
		}
	    $quality = ($quality - 100) / 11.11;
	    $quality = round(abs($quality));
    	parent::setQuality($quality);
    	return $this;
    }
}
?>

Ok, again let’s go over it from top to bottom. We start out by declaring the namespace again.
We extend our superclass Image, and define our mime type as a class constant.

In our constructor we call our superclass constructor including our mimetype.

The create function actually creates our new image. It takes a height and width parameter. Calls our parents createImage function to create the black image square and we fill that with our file.
We simply destroy the left over resource.

the show function simply set’s the correct content-type header and display our image.
The save function does not display but saves it, returns itself so it can be directory shown afterwards if required.

This is the basic, this can be used for JPEGs, PNGs, GIFs, BMPs everything. Small modifications are needed to the mime-type and also some of the functions (imagepng becomes imagejpeg etc. etc.).
But the reason I show you the PNG one is because it substitutes some of it’s parent functionality.
We have a function setQuality() inside our superclass but we overide it , since as we know from our previous code that the quality for a PNG is not based on a scale from 0 to 100, it is based on a scale from 9 to 0 (worst to best)

Now, to see it in action:

<?php
use Library\Image as Image;
require("Image.php");

try {
	$file = "image.png";

	// Image with be an isntance of PNG class
	$image = Image::factory($file);

	$image->setName("big")
	      //No need to use 0-9 here. the class itself knows what we want!
		  ->setQuality(90)
		  //We want to save it to the folder files
		  ->setDestination('files')
		  //save it with 1024 x 1024 dimensions
		  ->save(1024, 1024)
		  //Change the name, we will just chain it. since we want 3 versions of the file
		  ->setName("Medium")
		  //save it with 512 x 512 dimensions
		  ->save(512, 512)
		  //And the last name change
		  ->setName("small")
		  //save it with 256 x 256 dimensions
		  ->save(256, 256)
		  //And let's just show it
		  ->show(256, 256);
} catch (Exception $e) {
	echo "<pre>";
	print_r($e);
	echo "</pre>";
}
?>

This is an extreme case, where you want the same image in 3 different dimensions and in the end show the final image. As you can see you can chain all the commands together. The following would probably be more likely

<?php
$file = "image.png";
$image = Image::factory($file);
$image->setName("New Name")->save(1024, 1024);

I hope that was all pretty clear for the most of you, if you are actually using this I’d appreciate a comment. If there are any bugs or maybe bad design changes on my part. Please let me know, I am still learning every day, and these modification should show off a bit of the changes I have made over the past years.

28
Dec

How To: PHP Thumbnails – Resize images with PHP

I have revisited this subject here: Revisited: PHP Thumbnails

We all have been in situation where we allow users to upload images for content items or profile pictures. They’ll just upload huge images without considering bandwidth and disk space. Soon you have hundreds images above 2MB in size cluttering your precious harddisk space and taking up all of the bandwidth with just a few pageviews.

So how do you conquer this problem? Well, resize the images automatically. Keeping their ratio and quality but reducing their size significantly.

I assume you have some knowledge with HTML and know how to upload a file. Therefor in these examples I use the file example.png and assume its in the images/ folder.

First the class that we will use to resize the image

<?php
/**
 * Class createImage
 *
 * @author Thijs Damen
 * @desc Creates a resized image from an existing image
 *
 */
class createImage {

	/*Folder where the current image can be found*/
	private $RootFolder;

	/*Folder relative from RootFolder to write the image to*/
	private $dstFolder;

	/*Name of the image*/
	private $ImageName;

	/*Image Extension*/
	private $ImageExtention;

	/*Maximum width for image*/
	private $img_maxWidth;
	/*Maximum height for image*/
	private $img_maxHeight;

	/*Current width of the image*/
	private $img_curWidth;
	/*Current height of the image*/
	private $img_curHeight;

	/*New width of the image*/
	private $img_newWidth;

	/*New height of the image*/
	private $img_newHeight;

	/*Quality of the new image*/
	private $imgQuality;

	/*Thumb of the image*/
	private $thumb;

	/*
	 * Constructor
	 * Recieves all parameters and immediatly creates the new image
	 */
	public function __construct($rootfolder, $imagename, $dstfolder, $maxwidth, $maxheight, $quality = 100) {
		$this->RootFolder 		= 	$rootfolder;
		$this->ImageName		=	$imagename;
		$this->dstFolder		= 	$dstfolder;
									//Neat trick that will get everything from the last . as a string (aka. extension)
		$this->ImageExtention 	=   end(explode(".", $imagename));
		$this->img_maxWidth		= 	$maxwidth;
		$this->img_maxHeight	=	$maxheight;
		$this->imgQuality		=	$quality;

		$this->calcImgSize();
	}

	/*
	 * Smart function that will calculate the new width and height of the image
	 */
	private function calcImgSize() {
		$src = getimagesize($this->RootFolder.$this->ImageName);
		$this->img_curWidth 	= 	$src[0];
		$this->img_curHeight	= 	$src[1];

		//Some calculation to determine wether the layout it landscape or portrait
		if(($this->img_curWidth / $this->img_maxWidth) > ($this->img_curHeight / $this->img_maxHeight)) {
			// landscape mode so recalculate based on width
			if ($this->img_curWidth > $this->img_maxWidth) {
				$this->img_newWidth = intval($this->img_maxWidth);
				$this->img_newHeight = intval($this->img_curHeight * ($this->img_maxWidth / $this->img_curWidth));
			} else {
				$this->img_newWidth = $this->img_curWidth;
				$this->img_newHeight = $this->img_curHeight;
			}
		} else {
			// portrait mode so recaulculate on height
			if ($this->img_curHeight > $this->img_maxHeight) {
				$this->img_newHeight = intval($this->img_maxHeight);
				$this->img_newWidth = intval($this->img_curWidth * ($this->img_maxHeight / $this->img_curHeight));
			} else {
				$this->img_newWidth = $this->img_curWidth;
				$this->img_newHeight = $this->img_curHeight;
			}
		}
		//GD Function that returns an image identifier based on width and height
		$this->thumb = imagecreatetruecolor($this->img_newWidth, $this->img_newHeight);
		if (!$this->createImage()) {
			return false;
		}
	}

	/*
	 * Create the new image based on extension
	 */
	private function CreateImage() {
		/*
		 * Switch the extension. so we know what type of image we are dealing with
		 * All of these will use their respecitve create function (imagecreatefrom)
		 * Create a copy from it and write it to the disk
		 */
		switch ($this->ImageExtention) {
			case "jpg":
			case "jpeg":
				header("Content: image/jpeg");
				$imgcreate = imagecreatefromjpeg($this->RootFolder.$this->ImageName);
				$imgcopy = imagecopyresampled($this->thumb, $imgcreate, 0,0,0,0, $this->img_newWidth, $this->img_newHeight, $this->img_curWidth, $this->img_curHeight);
				return imagejpeg($this->thumb,$this->RootFolder.$this->dstFolder.$this->ImageName,$this->imgQuality);
			break;
			case "png":
				header("Content: image/png");
				$imgcreate = imagecreatefrompng($this->RootFolder.$this->ImageName);
				$imgcopy = imagecopyresampled($this->thumb, $imgcreate, 0,0,0,0, $this->img_newWidth, $this->img_newHeight, $this->img_curWidth, $this->img_curHeight);
				/*
				 *  This is a special case. Where imagejpeg and imagegif take their quality parameter as 0 to 100.
				 *  0 being the worst and 100 being best. imagepng expects 0 to 9, 0 being the best. 9 being the worst.
				 *  So we have to do some calculation for the quality
				 */
				$quality = ($this->imgQuality - 100) / 11.11;
				$this->imgQuality = round(abs($quality));
				return imagepng($this->thumb,$this->RootFolder.$this->dstFolder.$this->ImageName, $this->imgQuality);
			break;
			case "gif":
				header("Content: image/gif");
				$imgcreate = imagecreatefromgif($this->RootFolder.$this->ImageName);
				$imgcopy =  imagecopyresampled($this->thumb, $imgcreate, 0,0,0,0, $this->img_newWidth, $this->img_newHeight, $this->img_curWidth, $this->img_curHeight);
				return imagegif($this->thumb,$this->RootFolder.$this->dstFolder.$this->ImageName,$this->imgQuality);
			break;
		}
	}
}

How to use this class? Simple:

I have an image in the images/ folder named image.png. It is 4096×3072 pixels in size (and 1.4MB big) currently and I want to resize it to a maximum of 512×512.

new createImage("images/", "image.png", "thumbs/", 512, 512, 80);

This will now create a image in images/thumbs/, still named image.png of size: 512*384 with a little less quality then the original, but little chance you will actually notice the difference (also, it is only 56KB big, which is 2500% smaller(!) then the original).

The best part about this. I can do the same with my JPEGs and GIFs without a hassle. look

new createImage("images/","image.jpg", "thumbs/", 512, 512, 80);

And this will do exactly the same just for the JPG extension. So instead of having to remember 3 different functions, I can just use a single class which holds all the logic for me.

There are some prerequisites. The destFolder has to be writable and you have to have the GD library installed. Also, it might be necessary to temporarily increase the memory limit of php.

ini_set('memory_limit', '256M');

I hope this helps someone with resizing their images in a good way. Keeping proportions and reducing image size significantly.

20
Dec

How To: Create your own Content Distribution Network

There are many Content Distribution Networks (CDN) aka Content Delivery Networks out there so why create your own? I for one argue that most CDNs out there don’t host enough code.

Here’s a list that google supports:

Chrome Frame
Dojo
Ext Core
jQuery
jQuery UI
MooTools
Prototype
script.aculo.us
SWFObject
Yahoo! User Interface Library (YUI)
WebFont Loader

This may seem as a long list, but what if you use jQuery plugins like jQuery validate or Fancybox often?

Why not create your own CDN system to host the code you use the most (You can even choose to use googleapis for all the libraries that are available and use your own CDN for the ones that are not). Personally, I prefer that option.
Another benefit of hosting your own CDN is that if you create web applications using a in-house made CMS you can also host its icon-sets and other frequently accessed static images.

The reason’s to use a CDN are simple and straight-forward:

Using a CDN will save you bandwich
Using a CDN will greatly increase your scripts load speed

So how do you create your own CDN?

#1 Setup the domain

If it is possible create a new domain name and disable the use of cookies. Why? RavelRumba explain it in this post. If you can not afford, or do not want to pay for a new domain you can consider creating a subdomain and then making sure cookies are only saved on the www.* domain.

#2 Figure out a filetree structure

Your filetree is very important when setting up your CDN. Yes, you can simply put every file you need in the root directory of the webserver but this is clumbersome and after some time will become confusing.
Google uses the following syntax:

http://ajax.googleapis.com/ajax/libs/library/version/filename
or
http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js

You can also use this syntax or use a more simplified version. Which I personally prefer since the code you should host on your CDN often does not come with that many versions.

http://yourwebsite.com/library/filename

http://yourwebsite.com/library/plugins/pluginname/filename

http://yourwebsite.com/plugins/pluginname/filename

http://yourwebsite.com/images/imagename

I then use the library folder to host specific libraries (and their specific plugins), the plugins folder to host plugins (such as modernizr) and the image folder to host my frequently used images.

#3 Making it perform

As said before, you should disable cookies on the domain of your choice.

Because you are using a CDN you can use a custom .htaccess file to optimise the content distrubution. Set far-future (the content is static anyway) expire headers and deflate any content delivered.

.htaccess example (mostly stripped from the –awesome- html5boilerplate created by the, maybe even more awesome Paul Irish

#Set proper content-types for your files
# Proper svg serving.
AddType     image/svg+xml              svg svgz
AddEncoding gzip                       svgz           

# webfonts
AddType application/vnd.ms-fontobject  eot
AddType font/truetype                  ttf
AddType font/opentype                  otf
AddType font/woff                      woff          

# assorted types
AddType image/vnd.microsoft.icon       ico
AddType image/webp                     webp
AddType text/cache-manifest            manifest
AddType text/x-component               htc
AddType application/x-chrome-extension crx

# gzip compression.
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css application/json
  AddOutputFilterByType DEFLATE text/javascript application/javascript application/x-javascript
  AddOutputFilterByType DEFLATE text/xml application/xml text/x-component
  <FilesMatch "\.(ttf|otf|eot|svg)$" >
    SetOutputFilter DEFLATE
  </FilesMatch>
</IfModule>

<IfModule mod_expires.c>
  Header set Cache-Control "public"
  ExpiresActive on

# Perhaps better to whitelist expires rules? Perhaps.
  ExpiresDefault                          "access plus 1 month"

# cache.manifest needs re-requests in FF 3.6 (thx Remy ~Introducing HTML5)
  ExpiresByType text/cache-manifest       "access plus 0 seconds"

# favicon (cannot be renamed)
  ExpiresByType image/vnd.microsoft.icon  "access plus 1 week" 

# media: images, video, audio
  ExpiresByType image/gif                 "access plus 1 month"
  ExpiresByType image/png                 "access plus 1 month"
  ExpiresByType image/jpg                 "access plus 1 month"
  ExpiresByType image/jpeg                "access plus 1 month"
  ExpiresByType video/ogg                 "access plus 1 month"
  ExpiresByType audio/ogg                 "access plus 1 month"
  ExpiresByType video/mp4                 "access plus 1 month"
  ExpiresByType video/webm                "access plus 1 month"

# webfonts
  ExpiresByType font/truetype             "access plus 1 month"
  ExpiresByType font/opentype             "access plus 1 month"
  ExpiresByType font/woff                 "access plus 1 month"
  ExpiresByType image/svg+xml             "access plus 1 month"
  ExpiresByType application/vnd.ms-fontobject "access plus 1 month"

# css and javascript
  ExpiresByType text/css                  "access plus 1 week"
  ExpiresByType application/javascript    "access plus 1 week"
  ExpiresByType text/javascript           "access plus 1 week"
</IfModule>

# Since we're sending far-future expires, we don't need ETags for
# static content.
FileETag None

# use utf-8 encoding for anything served text/plain or text/html
AddDefaultCharset utf-8

# force utf-8 for a number of file formats
AddCharset utf-8 .html .css .js .xml .json .rss

#4 All done

It’s that simple to create a very basic CDN. Obviously there are other more advanced topics we could discuss about how to optimize the distribution of content, but this is in essence a CDN that does what it promises.