/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.css.compiler.passes; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.css.compiler.ast.CssCompilerPass; import com.google.common.css.compiler.ast.CssFunctionArgumentsNode; import com.google.common.css.compiler.ast.CssFunctionNode; import com.google.common.css.compiler.ast.CssFunctionNode.Function; import com.google.common.css.compiler.ast.CssHexColorNode; import com.google.common.css.compiler.ast.CssLiteralNode; import com.google.common.css.compiler.ast.CssNode; import com.google.common.css.compiler.ast.CssNumericNode; import com.google.common.css.compiler.ast.CssValueNode; import com.google.common.css.compiler.ast.DefaultTreeVisitor; import com.google.common.css.compiler.ast.MutatingVisitController; import java.util.List; import java.util.logging.Logger; /** * Compiler pass that optimizes color values. It shrinks 6-digit hex values to * 3-digit where possible, and converts rgb(r, g, b) to hex. * * @author oana@google.com (Oana Florescu) */ public class ColorValueOptimizer extends DefaultTreeVisitor implements CssCompilerPass { private static final Logger logger = Logger.getLogger( ColorValueOptimizer.class.getName()); private static final Function RGB = Function.byName("rgb"); private MutatingVisitController visitController; public ColorValueOptimizer(MutatingVisitController visitController) { this.visitController = visitController; } @Override public boolean enterFunctionNode(CssFunctionNode function) { if (function.getFunction() == RGB) { try { String hexValue = parseRgbArguments(function); if (canShortenHexString(hexValue)) { hexValue = shortenHexString(hexValue); } CssValueNode optimizedColor = new CssHexColorNode( hexValue, function.getSourceCodeLocation()); List<CssNode> temp = Lists.newArrayList(); temp.add(optimizedColor); visitController.replaceCurrentBlockChildWith(temp, true); } catch (NumberFormatException nfe) { logger.info("Error parsing rgb() function: " + nfe.toString()); } } return true; } @Override public boolean enterValueNode(CssValueNode node) { if (node instanceof CssHexColorNode) { CssHexColorNode color = (CssHexColorNode) node; if (canShortenHexString(color.getValue())) { String hexValue = shortenHexString(color.getValue()); CssValueNode optimizedColor = new CssHexColorNode( hexValue, node.getSourceCodeLocation()); List<CssNode> temp = Lists.newArrayList(); temp.add(optimizedColor); visitController.replaceCurrentBlockChildWith(temp, true); } } return true; } /** * Extract the rgb function arguments and convert them to a standard RGB * hex value. * @param function A function node. * @return The 6-digit hex value, including leading # sign. * @throws NumberFormatException when input is invalid. */ @VisibleForTesting static String parseRgbArguments(CssFunctionNode function) throws NumberFormatException { CssFunctionArgumentsNode args = function.getArguments(); int numArgs = 0; StringBuilder hexValue = new StringBuilder("#"); for (CssValueNode rgbValue : args.getChildren()) { if (rgbValue instanceof CssNumericNode) { numArgs++; CssNumericNode numericValue = (CssNumericNode) rgbValue; int scalarValue = Integer.parseInt(numericValue.getNumericPart()); if ("%".equals(numericValue.getUnit())) { scalarValue = (int) (255.0 * scalarValue / 100 + 0.5); } else if (!CssNumericNode.NO_UNITS.equals(numericValue.getUnit())) { throw new NumberFormatException("rgb arguments must be scalar or " + "%. Bad value:" + numericValue.toString()); } // According to W3C specs, out-of-range values are OK, but there's a // good chance it's unintentional, so emit a warning. if (scalarValue < 0) { logger.info("Out of range argument to rgb(): " + numericValue); scalarValue = 0; } if (scalarValue > 255) { logger.info("Out of range argument to rgb(): " + numericValue); scalarValue = 255; } if (scalarValue < 16) { hexValue.append('0'); } hexValue.append(Integer.toHexString(scalarValue)); } else if (rgbValue instanceof CssLiteralNode && ",".equals(rgbValue.getValue())) { // Sadly, the comma separators parse as function arguments, just // ignore and skip over them. } else { throw new NumberFormatException("Expected numeric value:" + rgbValue.getValue()); } } if (numArgs != 3) { throw new NumberFormatException("Invalid number of arguments to rgb()."); } return hexValue.toString(); } /** * Determine whether an RGB hex value can be abbreviated. * @param hex An RGB hex value, such as "#00ffcc". * @return Whether the value can be abbreviated. */ @VisibleForTesting static boolean canShortenHexString(String hex) { Preconditions.checkArgument(hex.startsWith("#")); return hex.length() == 7 && hex.charAt(1) == hex.charAt(2) && hex.charAt(3) == hex.charAt(4) && hex.charAt(5) == hex.charAt(6); } /** * Converts a 6-digit RGB hex value to its 3-digit equivalent. * This method assumes that {@link #canShortenHexString} has returned true. * @param hex Hex value, including leading "#". * @return 3-digit hex value, including leading "#". */ @VisibleForTesting static String shortenHexString(String hex) { StringBuilder optimizedHexValue = new StringBuilder("#"); optimizedHexValue.append(hex.charAt(1)); optimizedHexValue.append(hex.charAt(3)); optimizedHexValue.append(hex.charAt(5)); return optimizedHexValue.toString(); } @Override public void runPass() { visitController.startVisit(this); } }