Location: PHPKode > scripts > CSS Stacker and Compressor > css.class.php
<?php 
/*
                              CSStacker
***************************************************************************
Current version:
1.3
***************************************************************************

  css.class.php, a CSS optimizer and compressor
  http://unreal4u.com/

  Coded by unreal4u (Camilo Sperberg)
  Copyleft (c) 2010 Camilo Sperberg

  Released under the BSD License
*/

if (!isset($proc)) die('Sorry, direct access is not allowed!');

class CSStacker {
  private $files     = array();
  private $qCSS      = 0;
  private $filename  = '';
  private $lastmod   = '';
  public  $resetCSS  = FALSE;

// PUBLIC function that adds CSS files to the stack
  public function add($file) {
    if (is_array($file)) {
      foreach($file AS $a) 
        if ($this->verify_original_css($a)) $this->qCSS++;
    }
    else if ($this->verify_original_css($file)) $this->qCSS++;
  }

// PUBLIC function that does all the hard work
  public function printme($method = 'file',$force='') {
    if($this->qCSS > 0 AND ($method == 'file' OR $method == 'inline' OR $method == 'filename')) {
      if ($force == 'force') {
        $force = TRUE;
        $this->error('NOTICE','Cache creation is being forced!');
      }
      else $force = FALSE;
      $this->filename = $this->get_cache_filename();
      $this->lastmod  = time();
      $status = $this->status($method,$force);
      switch($status['act']) {
        CASE 1  : header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified', TRUE, 304);
                  $this->output_headers();
                  break;
        CASE 2  : ob_start();
                  header('Content-Type: text/css; charset='.CHARSET);
                  $this->output_headers();
                  if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) AND GZIP_CONTENTS) {
                    $enc = explode(',',str_replace(' ','',$_SERVER['HTTP_ACCEPT_ENCODING']));
                    if (in_array('gzip',$enc)) {
                      header('Content-Encoding: gzip');
                      echo gzencode($status['css'],GZIP_LEVEL);
                      return TRUE;
                    }
                  }
                  echo $status['css'];
                  header('Content-Length: '.ob_get_length());
                  ob_end_flush();
                  break;
        CASE 3  : echo $status['css'];
                  break;
        CASE 4  : return $status['css'];
                  break;
      }
      return 1;
    }
    return 0;
  }

/*
   PRIVATE function that verifies if original CSS's can be readable or not
*/
  private function verify_original_css($file) {
    $is_valid = TRUE;
    if (is_readable($file) AND !in_array($file,$this->files)) $this->files[] = $file;
    else {
      $is_valid = FALSE;
      if (!file_exists($file)) $this->error('WARNING','File "'.$file.'" doesn\'t exist');
      else $this->error('FATAL','File "'.$file.'" exists but I cannot read it. Permission problems?');
    }
    return $is_valid;
  }

/*
   PRIVATE function that does several checks, and based on that returns a status code and the corresponding CSS
*/
  private function status($method,$force) {
    $out = array('act' => '1', 'css' => FALSE);
    $cache_created = FALSE;
    $is_cache_valid = FALSE;
    if (USE_CSS_CACHE AND $force == FALSE) $is_cache_valid = $this->valid_cache();
    else $force = TRUE;

    if ($is_cache_valid AND $force == FALSE) {
      if ($method == 'file' AND USE_CSS_CACHE AND USE_BROWSER_CACHE AND isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->lastmod) return $out;
      elseif ($method == 'filename') return array('act' => '4', 'css' => $this->filename);
      elseif ($method == 'inline') return array('act' => '3', 'css' => $this->get_cache());
      else $out['act'] = 2;
    }
    elseif ($force == TRUE OR !$is_cache_valid) {
      $out['css'] = $this->compress();
      if ($method == 'file') $out['act'] = 2;
      elseif ($method == 'inline') $out['act'] = 3;
    }

    if ($out['css'] !== FALSE) $cache_created = $this->create_cache($out['css']);
    else $out['css'] = $this->get_cache();

    if ($method == 'filename') { 
      $out['act'] = 4;
      $out['css'] = $cache_created;
    }
    return $out;
  }

/*
    PRIVATE function that outputs the necesary browser cache headers
*/
  private function output_headers() {
    header('Last-Modified: '.gmdate('D, d M Y H:i:s',$this->lastmod).' GMT');
    header('Cache-Control: public, must-revalidate, max-age='.TIME_BROWSER_CACHE);
    header('Expires: '.gmdate('D, d M Y H:i:s',time() + TIME_BROWSER_CACHE).' GMT');
    return TRUE;
  }

/*
    PRIVATE function that checks if cache is still valid
*/
  private function valid_cache($check = NULL) {
    if (!USE_CSS_CACHE) return FALSE;
    $is_valid = TRUE;
    if (!is_readable($this->filename)) {
      $is_valid = FALSE;
      if (!file_exists($this->filename)) $this->error('NOTICE','Cache file doesn\'t exist.');
      else $this->error('FATAL','Couldn\'t read the cache file. Please check permissions!');
    }
    if ($is_valid)
      foreach($this->files AS $a)
        if (filemtime($a) > filemtime($this->filename) AND $is_valid)
          $is_valid = FALSE;
    if (!empty($check) OR !$is_valid) return $is_valid;
    else {
      $this->lastmod = filemtime($this->filename);
      return $is_valid;
    }
  }

/*
    PRIVATE function that creates or get an unique name for the cache based on original CSS filenames
*/
  private function get_cache_filename() {
    $filename = '';
    foreach($this->files AS $a) $filename .= $a;
    return CACHE_LOCATION.md5($filename).'.css';
  }

/*
    PRIVATE function that actually creates the cache file
*/
  private function create_cache($contents) {
    $created = FALSE;
    if (is_writable(CACHE_LOCATION)) {
      if (USE_CSS_CACHE) {
        file_put_contents($this->filename,$contents);
        $created = $this->filename;
      }
    }
    else $this->error('WARNING','Cache location isn\'t writable');
    return $created;
  }

/*
    PRIVATE function that rescues the cache content
*/
  private function get_cache() {
    $cache = FALSE;
    $recreate = FALSE;
    if (is_readable($this->filename)) {
      if (!$this->valid_cache(TRUE)) $recreate = TRUE;
      if ($recreate === FALSE) $cache = file_get_contents($this->filename);
      else {
        unlink($this->filename);
        $this->error('NOTICE','Cache file out-dated. Deleted so it can be recreated');
      }
    }
    else {
      if(!file_exists($this->filename)) $this->error('NOTICE','Cache file doesn\'t exist');
      else $this->error('WARNING','Cache file exists, but I could\'t read it. Permission or disk problems?');
    }
    return $cache;
  }

/*
    PRIVATE function that does several replaces in order to strip comments and remove some common unnecesary spaces
*/
  private function compress() {
    if ($this->resetCSS) $out = 'html{color:#000;background:#FFF}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}li{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:\'\'}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{*font-size:100%}legend{color:#000}';
      // CSS reset is taken from Yahoo developer and optimized/compressed:
      // http://developer.yahoo.com/yui/3/cssreset/
      // Original file is the Global one, its size is 501 bytes, compressed to 343 bytes.
    else $out = '';
    foreach($this->files AS $a) {
      $css_content = file_get_contents($a);
      if(strlen($css_content) > 94325) $tmp_buffer = str_split($css_content,94300);
      else $tmp_buffer[] = $css_content;
        // Why this? See: http://www.php.net/manual/en/function.preg-replace.php#93840
        // Note that around that area it is impossible to do a good optimization!
        // If you happen to have such big CSS files, it would be better to separate it into smaller ones
      unset($css_content);
      $tmp_buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!','',$tmp_buffer); // Strip out all comments
      $tmp_buffer = str_replace (array("\r\n", "\r", "\n", "\t", '  ', '    '),'',$tmp_buffer); // All enter types + additional white spaces
      if(OPTIMIZE_CSS === TRUE) $out .= $this->optimize($tmp_buffer);
      else {
        if(is_array($tmp_buffer)) {
          foreach($tmp_buffer AS $b) $out .= $b;
          unset($b);
        }
        else $out .= $tmp_buffer;
      }
      unset($tmp_buffer);
    }
    return $out;
  }

/*
    PRIVATE function that goes a little further replacing color codes and some common colors with their color codes
*/
  private function optimize($the_css) {
    $original = array(', ',' , ',';}','; }',' ; }',' :',': ',' {','; ', // Some common typos and unnecesary characters
                      ':black',':dark grey',':fuchsia',':light grey',':orange',':white',':yellow'); // Come common colors which are shorter in hex
    $replace  = array(',' ,','  ,'}' ,'}'  ,'}'   ,':' ,':' ,'{' ,';', // their replacements
                      ':#000' ,':#666'     ,':#F0F'   ,':#CCC'      ,':#F60'  ,':#FFF' ,':#FF0');

    $the_css = str_replace (array(' 0px',' 0em',' 0%',' 0ex',' 0cm',' 0mm',' 0in',' 0pt',' 0pc'),' 0',$the_css);
    $the_css = str_replace (array(':0px',':0em',':0%',':0ex',':0cm',':0mm',':0in',':0pt',':0pc'),':0',$the_css); // 0 needs no unit

    foreach($the_css AS $a) { // Color code optimization. Ex: #334455 -> #345
      $how_many = substr_count($a,'#');
      $offset = 0;
      for($i = 0; $i < $how_many; $i++) {
        $from = strpos($a,'#',$offset) + 1;
        $tmp = substr($a,$from,6);
        $k = strlen($tmp);
        for ($j = 0; $j < $k + 1; $j++) {
          if ($j % 2 != 0)
            if ($pre[$j - 1] == $tmp{$j}) $pre[] = TRUE;
            else $pre[] = FALSE;
          elseif ($j != $k) $pre[] = $tmp{$j};
        }
        if ($pre[1] AND $pre[3] AND $pre[5] AND !in_array('#'.$tmp,$original)) {
          $original[] = '#'.$tmp;
          $replace[]  = '#'.$pre[0].$pre[2].$pre[4];
        }
        unset($pre,$tmp,$j,$k);
        $offset = $from;
      }
    }
    $the_css = str_ireplace($original,$replace,$the_css);
    $out = '';
    foreach($the_css AS $a) $out .= $a;
    unset($original,$replace,$how_many,$offset,$from,$i,$a,$the_css);
    $out = implode('}',array_reverse(array_unique(array_reverse(explode('}',$out)))));
    // NOTE: This is beta! If you happen to have problems with the CSS, comment the above line
    //       What it does is stripping out repeated declarations, which means that:
    //   body{color:#000}p{background:#EEE}body{color:#000}
    //       will result in:
    //   p{background:#EEE}body{color:#000}
    return $out;
  }

/*
    PRIVATE function that keep a log on all errors or notices
*/
  private function error($errtype,$errmsg) {
    global $CSSErrors;
    $CSSErrors[] = array('type' => $errtype, 'errm' => $errmsg);
  }
}
?>
Return current item: CSS Stacker and Compressor