[css] Added compression and data uri capabilities to CSS files
authorDavid Baz Fayos <david.baz@openbravo.com>
Tue, 22 Mar 2011 14:44:53 +0100
changeset 11337 91fe001a8c53
parent 11336 77a6c2e1164a
child 11338 639512a95f28
child 11340 ac0f516bd3c1
[css] Added compression and data uri capabilities to CSS files
modules/org.openbravo.client.kernel/src/org/openbravo/client/kernel/CSSMinimizer.java
modules/org.openbravo.client.kernel/src/org/openbravo/client/kernel/StyleSheetResourceComponent.java
modules/org.openbravo.userinterface.smartclient/web/org.openbravo.userinterface.smartclient/openbravo/skins/3.00/smartclient/images/CubeGrid/colHeaderDown.gif
modules/org.openbravo.userinterface.smartclient/web/org.openbravo.userinterface.smartclient/openbravo/skins/3.00/smartclient/images/CubeGrid/rowHeaderDown.gif
src/index.jsp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/org.openbravo.client.kernel/src/org/openbravo/client/kernel/CSSMinimizer.java	Tue Mar 22 14:44:53 2011 +0100
@@ -0,0 +1,657 @@
+/**
+CSSMin Copyright License Agreement (BSD License)
+
+Copyright (c) 2011, Barry van Oudtshoorn
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above
+  copyright notice, this list of conditions and the
+        following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the
+        following disclaimer in the documentation and/or other
+        materials provided with the distribution.
+
+ * Neither the name of Barryvan nor the names of its
+  contributors may be used to endorse or promote products
+        derived from this software without specific prior
+        written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.openbravo.client.kernel;
+
+import java.io.BufferedReader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.log4j.Logger;
+
+/**
+ * 
+ * CSSMinimizer takes in well-formed, human-readable CSS and reduces its size substantially. It
+ * removes unnecessary whitespace and comments, and orders the contents of CSS selectors
+ * alphabetically to enhance GZIP compression.
+ * 
+ * Originally by Barry van Oudtshoorn, with bug reports, fixes, and contributions by
+ * <ul>
+ * <li>Kevin de Groote</li>
+ * <li>Pedro Pinheiro</li>
+ * </ul>
+ * Some code is based on the YUI CssCompressor code, by Julien Lecomte.
+ * 
+ * @author Barry van Oudtshoorn
+ */
+public class CSSMinimizer {
+
+  private static Logger log = Logger.getLogger(CSSMinimizer.class);
+
+  /**
+   * Minify CSS from a reader to a printstream.
+   * 
+   * @param input
+   *          Where to read the CSS from
+   * @param out
+   *          Where to write the result to
+   */
+  public static String formatString(String input) {
+    StringBuffer generatedSB = new StringBuffer();
+    try {
+      int k, j, // Number of open braces
+      n; // Current position in stream
+      char curr;
+
+      BufferedReader br = new BufferedReader(new StringReader(input));
+      StringBuffer sb = new StringBuffer();
+
+      log.debug("Reading file into StringBuffer...");
+
+      String s;
+      while ((s = br.readLine()) != null) {
+        if (s.trim().equals(""))
+          continue;
+        sb.append(s);
+      }
+
+      log.debug("Removing comments...");
+
+      // Find the start of the comment
+      while ((n = sb.indexOf("/*")) != -1) {
+        if (sb.charAt(n + 2) == '*') { // Retain special comments
+          n += 2;
+          continue;
+        }
+        k = sb.indexOf("*/", n + 2);
+        if (k == -1) {
+          throw new Exception("Unterminated comment. Aborting.");
+        }
+        sb.delete(n, k + 2);
+      }
+      if (log.isDebugEnabled()) {
+        log.debug(sb.toString());
+        log.debug("\n\n");
+      }
+
+      log.debug("Parsing and processing selectors...");
+
+      Vector<Selector> selectors = new Vector<Selector>();
+      n = 0;
+      j = 0;
+      k = 0;
+      for (int i = 0; i < sb.length(); i++) {
+        curr = sb.charAt(i);
+        if (j < 0) {
+          throw new Exception("Unbalanced braces!");
+        }
+        if (curr == '{') {
+          j++;
+        } else if (curr == '}') {
+          j--;
+          if (j == 0) {
+            try {
+              selectors.addElement(new Selector(sb.substring(n, i + 1)));
+            } catch (Exception e) {
+              if (e.getMessage().contains("Empty selector body:")) {
+                log.warn(e.getMessage());
+              } else {
+                log.error(e.getMessage(), e);
+              }
+            }
+            n = i + 1;
+          }
+        }
+      }
+
+      for (Selector selector : selectors) {
+        generatedSB.append(selector.toString());
+      }
+      generatedSB.append("\r\n");
+
+      log.debug("Process completed successfully.");
+
+    } catch (Exception e) {
+      log.debug(e.getMessage(), e);
+    }
+    return generatedSB.toString();
+  }
+}
+
+class Selector {
+  private Property[] properties = null;
+  private Vector<Selector> subSelectors = null;
+  private String selector;
+  private static final Logger log = Logger.getLogger(Selector.class);
+
+  /**
+   * Creates a new Selector using the supplied strings.
+   * 
+   * @param selector
+   *          The selector; for example, "div { border: solid 1px red; color: blue; }"
+   * @throws Exception
+   *           If the selector is incomplete and cannot be parsed.
+   */
+  public Selector(String selector) throws Exception {
+    String[] parts = selector.split("\\{"); // We have to escape the { with a \ for the regex, which
+    // itself requires escaping for the string. Sigh.
+    if (parts.length < 2) {
+      throw new Exception("Warning: Incomplete selector: " + selector);
+    }
+
+    this.selector = parts[0].toString().trim();
+
+    // Simplify combinators
+    this.selector = this.selector.replaceAll("\\s?(\\+|~|,|=|~=|\\^=|\\$=|\\*=|\\|=|>)\\s?", "$1");
+
+    // We're dealing with a nested property, eg @-webkit-keyframes
+    if (parts.length > 2) {
+      this.subSelectors = new Vector<Selector>();
+      parts = selector.split("\\{|\\}");
+      for (int i = 1; i < parts.length; i += 2) {
+        parts[i] = parts[i].trim();
+        parts[i + 1] = parts[i + 1].trim();
+        if (!(parts[i].equals("") || (parts[i + 1].equals("")))) {
+          this.subSelectors.addElement(new Selector(parts[i] + "{" + parts[i + 1] + "}"));
+        }
+      }
+    } else {
+      String contents = parts[parts.length - 1].trim();
+      if (log.isDebugEnabled()) {
+        log.debug("Parsing selector: " + this.selector);
+        log.debug("\t" + contents);
+      }
+      if (contents.charAt(contents.length() - 1) != '}') { // Ensure that we have a leading and
+        // trailing brace.
+        throw new Exception("\tUnterminated selector: " + selector);
+      }
+      if (contents.length() == 1) {
+        throw new Exception("\tEmpty selector body: " + selector);
+      }
+      contents = contents.substring(0, contents.length() - 2);
+      this.properties = parseProperties(contents);
+      sortProperties(this.properties);
+    }
+  }
+
+  /**
+   * Prints out this selector and its contents nicely, with the contents sorted alphabetically.
+   * 
+   * @returns A string representing this selector, minified.
+   */
+  public String toString() {
+    StringBuffer sb = new StringBuffer();
+    sb.append(this.selector).append("{");
+    if (this.subSelectors != null) {
+      for (Selector s : this.subSelectors) {
+        sb.append(s.toString());
+      }
+    }
+    if (this.properties != null) {
+      for (Property p : this.properties) {
+        sb.append(p.toString());
+      }
+    }
+    if (sb.charAt(sb.length() - 1) == ';') {
+      sb.deleteCharAt(sb.length() - 1);
+    }
+    sb.append("}");
+    return sb.toString();
+  }
+
+  /**
+   * Parses out the properties of a selector's body.
+   * 
+   * @param contents
+   *          The body; for example, "border: solid 1px red; color: blue;"
+   * @returns An array of properties parsed from this selector.
+   */
+  private Property[] parseProperties(String contents) {
+    ArrayList<String> parts = new ArrayList<String>();
+    boolean bCanSplit = true;
+    int j = 0;
+    String substr;
+    for (int i = 0; i < contents.length(); i++) {
+      if (!bCanSplit) { // If we're inside a string
+        bCanSplit = (contents.charAt(i) == '"');
+      } else if (contents.charAt(i) == '"') {
+        bCanSplit = false;
+      } else if (contents.charAt(i) == ';') {
+        substr = contents.substring(j, i);
+        if (!(substr.trim().equals("") || (substr == null)))
+          parts.add(substr);
+        j = i + 1;
+      }
+    }
+    substr = contents.substring(j, contents.length());
+    if (!(substr.trim().equals("") || (substr == null)))
+      parts.add(substr);
+    Property[] results = new Property[parts.size()];
+
+    for (int i = 0; i < parts.size(); i++) {
+      try {
+        results[i] = new Property(parts.get(i));
+      } catch (Exception e) {
+        log.error(e.getMessage(), e);
+        results[i] = null;
+      }
+    }
+
+    return results;
+  }
+
+  /**
+   * Sorts the properties array to enhance gzipping.
+   * 
+   * @param properties1
+   *          The array to be sorted.
+   */
+  private void sortProperties(Property[] props) {
+    Arrays.sort(props);
+  }
+}
+
+class Property implements Comparable<Property> {
+  protected String property;
+  protected Part[] parts;
+  private static final Logger log = Logger.getLogger(Property.class);
+
+  /**
+   * Creates a new Property using the supplied strings. Parses out the values of the property
+   * selector.
+   * 
+   * @param property
+   *          The property; for example, "border: solid 1px red;" or
+   *          "-moz-box-shadow: 3px 3px 3px rgba(255, 255, 0, 0.5);".
+   * @throws Exception
+   *           If the property is incomplete and cannot be parsed.
+   */
+  public Property(String property) throws Exception {
+    try {
+      // Parse the property.
+      ArrayList<String> _parts = new ArrayList<String>();
+      boolean bCanSplit = true;
+      int j = 0;
+      String substr;
+
+      log.debug("\t\tExamining property: " + property);
+
+      for (int i = 0; i < property.length(); i++) {
+        if (!bCanSplit) { // If we're inside a string
+          bCanSplit = (property.charAt(i) == '"');
+        } else if (property.charAt(i) == '"') {
+          bCanSplit = false;
+        } else if (property.charAt(i) == ':') {
+          substr = property.substring(j, i);
+          if (!(substr.trim().equals("") || (substr == null)))
+            _parts.add(substr);
+          j = i + 1;
+        }
+      }
+      substr = property.substring(j, property.length());
+      if (!(substr.trim().equals("") || (substr == null)))
+        _parts.add(substr);
+      if (_parts.size() < 2) {
+        throw new Exception("\t\tWarning: Incomplete property: " + property);
+      }
+      this.property = _parts.get(0).trim().toLowerCase();
+
+      this.parts = parseValues(simplifyColours(_parts.get(1).trim().replaceAll(", ", ",")));
+
+    } catch (PatternSyntaxException e) {
+      // Invalid regular expression used.
+    }
+  }
+
+  /**
+   * Prints out this property nicely.
+   * 
+   * @returns A string representing this property, minified.
+   */
+  public String toString() {
+    StringBuffer sb = new StringBuffer();
+    sb.append(this.property).append(":");
+    for (Part p : this.parts) {
+      sb.append(p.toString()).append(",");
+    }
+    sb.deleteCharAt(sb.length() - 1); // Delete the trailing comma.
+    sb.append(";");
+    if (log.isDebugEnabled()) {
+      log.debug(sb.toString());
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Compare this property with another.
+   */
+  public int compareTo(Property other) {
+    // We can't just use String.compareTo(), because we need to sort properties that have hack
+    // prefixes last -- eg, *display should come after display.
+    String thisProp = this.property;
+    String thatProp = other.property;
+
+    if (thisProp.charAt(0) == '-') {
+      thisProp = thisProp.substring(1);
+      thisProp = thisProp.substring(thisProp.indexOf('-') + 1);
+    } else if (thisProp.charAt(0) < 65) {
+      thisProp = thisProp.substring(1);
+    }
+
+    if (thatProp.charAt(0) == '-') {
+      thatProp = thatProp.substring(1);
+      thatProp = thatProp.substring(thatProp.indexOf('-') + 1);
+    } else if (thatProp.charAt(0) < 65) {
+      thatProp = thatProp.substring(1);
+    }
+
+    return thisProp.compareTo(thatProp);
+  }
+
+  /**
+   * Parse the values out of a property.
+   * 
+   * @param contents
+   *          The property to parse
+   * @returns An array of Parts
+   */
+  private Part[] parseValues(String contents) {
+    String[] _parts = contents.split(",");
+    Part[] results = new Part[_parts.length];
+
+    for (int i = 0; i < _parts.length; i++) {
+      try {
+        results[i] = new Part(_parts[i]);
+      } catch (Exception e) {
+        log.error(e.getMessage(), e);
+        results[i] = null;
+      }
+    }
+
+    return results;
+  }
+
+  private String simplifyColours(String contents) {
+    // This replacement, although it results in a smaller uncompressed file,
+    // actually makes the gzipped file bigger -- people tend to use rgba(0,0,0,0.x)
+    // quite a lot, which means that rgba(0,0,0,0) has its first eight or so characters
+    // compressed really efficiently; much more so than "transparent".
+    // contents = contents.replaceAll("rgba\\(0,0,0,0\\)", "transparent");
+
+    return simplifyRGBColours(contents);
+  }
+
+  // Convert rgb(51,102,153) to #336699 (this code largely based on YUI code)
+  private String simplifyRGBColours(String contents) {
+    StringBuffer newContents = new StringBuffer();
+    StringBuffer hexColour;
+    String[] rgbColours;
+    int colourValue;
+
+    Pattern pattern = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
+    Matcher matcher = pattern.matcher(contents);
+
+    while (matcher.find()) {
+      hexColour = new StringBuffer("#");
+      rgbColours = matcher.group(1).split(",");
+      for (int i = 0; i < rgbColours.length; i++) {
+        colourValue = Integer.parseInt(rgbColours[i]);
+        if (colourValue < 16) {
+          hexColour.append("0");
+        }
+        hexColour.append(Integer.toHexString(colourValue));
+      }
+      matcher.appendReplacement(newContents, hexColour.toString());
+    }
+    matcher.appendTail(newContents);
+
+    return newContents.toString();
+  }
+}
+
+class Part {
+  String contents;
+
+  /**
+   * Create a new property by parsing the given string.
+   * 
+   * @param contents
+   *          The string to parse.
+   * @throws Exception
+   *           If the part cannot be parsed.
+   */
+  public Part(String contents) throws Exception {
+    // Many of these regular expressions are adapted from those used in the YUI CSS Compressor.
+
+    // For simpler regexes.
+    this.contents = " " + contents;
+
+    simplify();
+  }
+
+  private void simplify() {
+    // !important doesn't need to be spaced
+    this.contents = this.contents.replaceAll(" !important", "!important");
+
+    // Replace 0in, 0cm, etc. with just 0
+    this.contents = this.contents.replaceAll("(\\s)(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2");
+
+    // Replace 0.6 with .6
+    // Disabled, as it actually makes compression worse! People use rgba(0,0,0,0) and
+    // rgba(0,0,0,0.x) a lot.
+    // this.contents = this.contents.replaceAll("(\\s)0+\\.(\\d+)", "$1.$2");
+
+    this.contents = this.contents.trim();
+
+    // Simplify multiple zeroes
+    if (this.contents.equals("0 0 0 0"))
+      this.contents = "0";
+    if (this.contents.equals("0 0 0"))
+      this.contents = "0";
+    if (this.contents.equals("0 0"))
+      this.contents = "0";
+
+    // Simplify multiple-parameter properties
+    simplifyParameters();
+
+    // Simplify font weights
+    simplifyFontWeights();
+
+    // Strip unnecessary quotes from url() and single-word parts, and make as much lowercase as
+    // possible.
+    simplifyQuotesAndCaps();
+
+    // Simplify colours
+    simplifyColourNames();
+    simplifyHexColours();
+  }
+
+  private void simplifyParameters() {
+    StringBuffer newContents = new StringBuffer();
+
+    String[] params = this.contents.split(" ");
+    if (params.length == 4) {
+      // We can drop off the fourth item if the second and fourth items match
+      // ie turn 3px 0 3px 0 into 3px 0 3px
+      if (params[1].equalsIgnoreCase(params[3])) {
+        params = Arrays.copyOf(params, 3);
+      }
+    }
+    if (params.length == 3) {
+      // We can drop off the third item if the first and third items match
+      // ie turn 3px 0 3px into 3px 0
+      if (params[0].equalsIgnoreCase(params[2])) {
+        params = Arrays.copyOf(params, 2);
+      }
+    }
+    if (params.length == 2) {
+      // We can drop off the second item if the first and second items match
+      // ie turn 3px 3px into 3px
+      if (params[0].equalsIgnoreCase(params[1])) {
+        params = Arrays.copyOf(params, 1);
+      }
+    }
+
+    for (int i = 0; i < params.length; i++) {
+      newContents.append(params[i] + " ");
+    }
+    newContents.deleteCharAt(newContents.length() - 1); // Delete the trailing space
+
+    this.contents = newContents.toString();
+  }
+
+  private void simplifyFontWeights() {
+    String lcContents = this.contents.toLowerCase();
+
+    for (int i = 0; i < Constants.fontWeightNames.length; i++) {
+      if (lcContents.equals(Constants.fontWeightNames[i])) {
+        this.contents = Constants.fontWeightValues[i];
+        break;
+      }
+    }
+  }
+
+  private void simplifyQuotesAndCaps() {
+    // Strip quotes from URLs
+    if ((this.contents.length() > 4) && (this.contents.substring(0, 4).equalsIgnoreCase("url("))) {
+      this.contents = this.contents.replaceAll("(?i)url\\(('|\")?(.*?)\\1\\)", "url($2)");
+    } else {
+      String[] words = this.contents.split("\\s");
+      if (words.length == 1) {
+        this.contents = this.contents.toLowerCase();
+        this.contents = this.contents.replaceAll("('|\")?(.*?)\1", "$2");
+      }
+    }
+  }
+
+  private void simplifyColourNames() {
+    String lcContents = this.contents.toLowerCase();
+
+    for (int i = 0; i < Constants.htmlColourNames.length; i++) {
+      if (lcContents.equals(Constants.htmlColourNames[i])) {
+        if (Constants.htmlColourValues[i].length() < Constants.htmlColourNames[i].length()) {
+          this.contents = Constants.htmlColourValues[i];
+        }
+        break;
+      } else if (lcContents.equals(Constants.htmlColourValues[i])) {
+        if (Constants.htmlColourNames[i].length() < Constants.htmlColourValues[i].length()) {
+          this.contents = Constants.htmlColourNames[i];
+        }
+      }
+    }
+  }
+
+  private void simplifyHexColours() {
+    StringBuffer newContents = new StringBuffer();
+
+    Pattern pattern = Pattern
+        .compile("#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])");
+    Matcher matcher = pattern.matcher(this.contents);
+
+    while (matcher.find()) {
+      if (matcher.group(1).equalsIgnoreCase(matcher.group(2))
+          && matcher.group(3).equalsIgnoreCase(matcher.group(4))
+          && matcher.group(5).equalsIgnoreCase(matcher.group(6))) {
+        matcher.appendReplacement(newContents, "#" + matcher.group(1).toLowerCase()
+            + matcher.group(3).toLowerCase() + matcher.group(5).toLowerCase());
+      } else {
+        matcher.appendReplacement(newContents, matcher.group().toLowerCase());
+      }
+    }
+    matcher.appendTail(newContents);
+
+    this.contents = newContents.toString();
+  }
+
+  /**
+   * Returns itself.
+   * 
+   * @returns this part's string representation.
+   */
+  public String toString() {
+    return this.contents;
+  }
+}
+
+class Constants {
+  static final String[] htmlColourNames = { "aliceblue", "antiquewhite", "aqua", "aquamarine",
+      "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
+      "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk",
+      "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen",
+      "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
+      "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkturquoise",
+      "darkviolet", "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", "floralwhite",
+      "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green",
+      "greenyellow", "honeydew", "hotpink", "indianred ", "indigo ", "ivory", "khaki", "lavender",
+      "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan",
+      "lightgoldenrodyellow", "lightgrey", "lightgreen", "lightpink", "lightsalmon",
+      "lightseagreen", "lightskyblue", "lightslategray", "lightsteelblue", "lightyellow", "lime",
+      "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
+      "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+      "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite",
+      "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod",
+      "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink",
+      "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon",
+      "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue",
+      "slategray", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato",
+      "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen" };
+
+  static final String[] htmlColourValues = { "#f0f8ff", "#faebd7", "#00ffff", "#7fffd4", "#f0ffff",
+      "#f5f5dc", "#ffe4c4", "#000", "#ffebcd", "#00f", "#8a2be2", "#a52a2a", "#deb887", "#5f9ea0",
+      "#7fff00", "#d2691e", "#ff7f50", "#6495ed", "#fff8dc", "#dc143c", "#0ff", "#00008b",
+      "#008b8b", "#b8860b", "#a9a9a9", "#006400", "#bdb76b", "#8b008b", "#556b2f", "#ff8c00",
+      "#9932cc", "#8b0000", "#e9967a", "#8fbc8f", "#483d8b", "#2f4f4f", "#00ced1", "#9400d3",
+      "#ff1493", "#00bfff", "#696969", "#1e90ff", "#b22222", "#fffaf0", "#228b22", "#f0f",
+      "#dcdcdc", "#f8f8ff", "#ffd700", "#daa520", "#808080", "#008000", "#adff2f", "#f0fff0",
+      "#ff69b4", "#cd5c5c", "#4b0082", "#fffff0", "#f0e68c", "#e6e6fa", "#fff0f5", "#7cfc00",
+      "#fffacd", "#add8e6", "#f08080", "#e0ffff", "#fafad2", "#d3d3d3", "#90ee90", "#ffb6c1",
+      "#ffa07a", "#20b2aa", "#87cefa", "#789", "#b0c4de", "#ffffe0", "#0f0", "#32cd32", "#faf0e6",
+      "#f0f", "#800000", "#66cdaa", "#0000cd", "#ba55d3", "#9370d8", "#3cb371", "#7b68ee",
+      "#00fa9a", "#48d1cc", "#c71585", "#191970", "#f5fffa", "#ffe4e1", "#ffe4b5", "#ffdead",
+      "#000080", "#fdf5e6", "#808000", "#6b8e23", "#ffa500", "#ff4500", "#da70d6", "#eee8aa",
+      "#98fb98", "#afeeee", "#d87093", "#ffefd5", "#ffdab9", "#cd853f", "#ffc0cb", "#dda0dd",
+      "#b0e0e6", "#800080", "#f00", "#bc8f8f", "#4169e1", "#8b4513", "#fa8072", "#f4a460",
+      "#2e8b57", "#fff5ee", "#a0522d", "#c0c0c0", "#87ceeb", "#6a5acd", "#708090", "#fffafa",
+      "#00ff7f", "#4682b4", "#d2b48c", "#008080", "#d8bfd8", "#ff6347", "#40e0d0", "#ee82ee",
+      "#f5deb3", "#fff", "#f5f5f5", "#ff0", "#9acd32" };
+  static final String[] fontWeightNames = { "normal", "bold", "bolder", "lighter" };
+  static final String[] fontWeightValues = { "400", "700", "900", "100" };
+}
\ No newline at end of file
--- a/modules/org.openbravo.client.kernel/src/org/openbravo/client/kernel/StyleSheetResourceComponent.java	Mon Mar 21 14:02:53 2011 +0100
+++ b/modules/org.openbravo.client.kernel/src/org/openbravo/client/kernel/StyleSheetResourceComponent.java	Tue Mar 22 14:44:53 2011 +0100
@@ -18,7 +18,9 @@
  */
 package org.openbravo.client.kernel;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.StringReader;
 import java.util.List;
 
 import javax.enterprise.inject.Any;
@@ -26,6 +28,7 @@
 import javax.inject.Inject;
 import javax.servlet.ServletContext;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.log4j.Logger;
@@ -41,6 +44,7 @@
  */
 public class StyleSheetResourceComponent extends BaseComponent {
   private static final Logger log = Logger.getLogger(StyleSheetResourceComponent.class);
+  private static final String IMGURLHOLDER = "__URLHOLDER__";
 
   @Inject
   @Any
@@ -103,7 +107,11 @@
         || !getParameters().get(KernelConstants.MODE_PARAMETER).equals(
             KernelConstants.MODE_PARAMETER_300);
 
+    final boolean makeCssDataUri = getParameters().get("_cssDataUri") != null
+        && getParameters().get("_cssDataUri").equals("true");
+
     final String skinParam;
+
     if (classicMode) {
       skinParam = KernelConstants.SKIN_VERSION_CLASSIC;
     } else {
@@ -136,27 +144,76 @@
               }
 
               try {
-                final File file = new File(context.getRealPath(resourcePath));
+                final String realResourcePath = context.getRealPath(resourcePath);
+                final File file = new File(realResourcePath);
                 if (!file.exists() || !file.canRead()) {
                   log.error(file.getAbsolutePath() + " cannot be read");
                   continue;
                 }
-                String resourceContents = FileUtils.readFileToString(file);
+                String resourceContents = FileUtils.readFileToString(file, "UTF-8");
 
-                final int lastIndex = resourcePath.lastIndexOf("/");
-                final String path = getContextUrl() + resourcePath.substring(0, lastIndex);
+                final String contextPath = getContextUrl()
+                    + resourcePath.substring(0, resourcePath.lastIndexOf("/"));
+                final String realPath = realResourcePath.substring(0,
+                    realResourcePath.lastIndexOf("/"));
 
                 // repair urls
-                resourceContents = resourceContents.replace("url(./", "url(" + path + "/");
+                resourceContents = resourceContents.replace("url(./", "url(" + IMGURLHOLDER + "/");
+                resourceContents = resourceContents.replace("url(images", "url(" + IMGURLHOLDER
+                    + "/images");
+                resourceContents = resourceContents.replace("url(\"images", "url(\"" + IMGURLHOLDER
+                    + "/images");
+                resourceContents = resourceContents.replace("url('images", "url('" + IMGURLHOLDER
+                    + "/images");
                 resourceContents = resourceContents
-                    .replace("url(images", "url(" + path + "/images");
-                resourceContents = resourceContents.replace("url(\"images", "url(\"" + path
-                    + "/images");
-                resourceContents = resourceContents.replace("url('images", "url('" + path
-                    + "/images");
-                resourceContents = resourceContents.replace("url('./", "url('" + path + "/");
-                resourceContents = resourceContents.replace("url(\"./", "url(\"" + path + "/");
+                    .replace("url('./", "url('" + IMGURLHOLDER + "/");
+                resourceContents = resourceContents.replace("url(\"./", "url(\"" + IMGURLHOLDER
+                    + "/");
 
+                if (!module.isInDevelopment()) {
+                  resourceContents = CSSMinimizer.formatString(resourceContents);
+                  if (makeCssDataUri) {
+                    String resourceContentsLine;
+                    BufferedReader resourceContentsReader = new BufferedReader(new StringReader(
+                        resourceContents));
+                    StringBuffer resourceContentsBuffer = new StringBuffer();
+
+                    int indexOfUrl;
+                    String imgUrl, imgExt, imgDataUri, newUrlParam;
+                    while ((resourceContentsLine = resourceContentsReader.readLine()) != null) {
+                      indexOfUrl = 0;
+                      while ((indexOfUrl = resourceContentsLine.indexOf("url(", indexOfUrl)) != -1) {
+                        imgUrl = resourceContentsLine.substring(indexOfUrl + 4,
+                            resourceContentsLine.indexOf(")", indexOfUrl));
+                        if (imgUrl.indexOf("\"") == 0 || imgUrl.indexOf("'") == 0) {
+                          imgUrl = imgUrl.substring(1, imgUrl.length());
+                        }
+                        if (imgUrl.indexOf("\"") == imgUrl.length() - 1
+                            || imgUrl.indexOf("'") == imgUrl.length() - 1) {
+                          imgUrl = imgUrl.substring(0, imgUrl.length() - 1);
+                        }
+                        imgExt = imgUrl.substring(imgUrl.lastIndexOf(".") + 1, imgUrl.length());
+                        imgExt = imgExt.toLowerCase();
+                        if (imgExt.equals("jpg")) {
+                          imgExt = "jpeg";
+                        }
+                        if (imgExt.equals("jpeg") || imgExt.equals("png") || imgExt.equals("gif")) {
+                          imgDataUri = filePathToBase64(imgUrl.replace(IMGURLHOLDER, realPath));
+                        } else {
+                          imgDataUri = "";
+                        }
+                        if (imgDataUri != "") {
+                          newUrlParam = "data:image/" + imgExt + ";base64," + imgDataUri;
+                          resourceContentsLine = resourceContentsLine.replace(imgUrl, newUrlParam);
+                        }
+                        indexOfUrl = indexOfUrl + 1;
+                      }
+                      resourceContentsBuffer.append(resourceContentsLine).append("\n");
+                    }
+                    resourceContents = resourceContentsBuffer.toString();
+                  }
+                }
+                resourceContents = resourceContents.replace(IMGURLHOLDER, contextPath);
                 sb.append(resourceContents);
               } catch (Exception e) {
                 log.error("Error reading file: " + resource, e);
@@ -173,4 +230,18 @@
   public String getId() {
     return KernelConstants.STYLE_SHEET_COMPONENT_ID;
   }
+
+  private String filePathToBase64(String path) {
+    try {
+      final File f = new File(path);
+      if (!f.exists() || !f.canRead()) {
+        return "";
+      }
+      byte[] fileBase64Bytes = Base64.encodeBase64(FileUtils.readFileToByteArray(f));
+      return new String(fileBase64Bytes);
+    } catch (final Exception e) {
+      log.error("Error processing file: " + path + " - " + e.getMessage(), e);
+    }
+    return "";
+  }
 }
Binary file modules/org.openbravo.userinterface.smartclient/web/org.openbravo.userinterface.smartclient/openbravo/skins/3.00/smartclient/images/CubeGrid/colHeaderDown.gif has changed
Binary file modules/org.openbravo.userinterface.smartclient/web/org.openbravo.userinterface.smartclient/openbravo/skins/3.00/smartclient/images/CubeGrid/rowHeaderDown.gif has changed
--- a/src/index.jsp	Mon Mar 21 14:02:53 2011 +0100
+++ b/src/index.jsp	Tue Mar 22 14:44:53 2011 +0100
@@ -43,6 +43,26 @@
 if(userId == null){
   return;
 }
+
+String ua = request.getHeader( "User-Agent" );
+boolean isMSIE = ( ua != null && ua.indexOf( "MSIE" ) != -1 );
+int verMSIE = 0;
+String verMSIEtmp = "";
+if (isMSIE) {
+  verMSIEtmp = ua.substring(ua.indexOf("MSIE") + 5);
+  verMSIEtmp = verMSIEtmp.substring(0, verMSIEtmp.indexOf("."));
+  if (ua.indexOf("MSIE 7.0") != -1 && ua.indexOf("Trident/4") != -1) {
+    //In case IE8 runs in "IE8 Compatibility mode, look for Trident/4.0 to know that is IE8 although MSIE string is MSIE 7.0
+    verMSIEtmp = "8";
+  } else if (ua.indexOf("MSIE 7.0") != -1 && ua.indexOf("Trident/5") != -1) {
+    // In case IE9 runs in "IE8 Compatibility mode, look for Trident/5.0 to know that is IE9 although MSIE string is MSIE 7.0
+    verMSIEtmp = "9";
+  } else if (ua.indexOf("MSIE 7.0") != -1 && ua.indexOf("Trident/") != -1) {
+    // For hypothetic future IE versions in case IEX runs in "IEX Compatibility mode, look for Trident/ to know that is IEX although MSIE string is MSIE 7.0
+    verMSIEtmp = "10"; //If this 'if' statement is not updated, could be 10 or 11 or anything... but set 10 just to ensure it is not in IE7
+  }
+  verMSIE = Integer.parseInt(verMSIEtmp);
+}
 %>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
         "http://www.w3.org/TR/html4/loose.dtd">
@@ -57,9 +77,11 @@
 <meta name="keywords" content="openbravo">
 <meta name="description" content="Openbravo S.L.U.">
 <link rel="shortcut icon" href="./web/images/favicon.ico" />
-<link rel="stylesheet" type="text/css" href="./org.openbravo.client.kernel/OBCLKER_Kernel/StyleSheetResources?_mode=3.00&_skinVersion=3.00"/>
+<link rel="stylesheet" type="text/css" href="./org.openbravo.client.kernel/OBCLKER_Kernel/StyleSheetResources?_mode=3.00&_skinVersion=3.00&_cssDataUri=<%=(!isMSIE || (isMSIE && verMSIE >=8))%>"/>
+
 <title>Openbravo</title>
 <script type="text/javascript" src="./web/org.openbravo.client.kernel/js/LAB.min.js"></script>
+
 <!-- styles used during loading -->
 <style type="text/css">
   html, body {