com.google.common.css.compiler.passes.ColorValueOptimizer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.css.compiler.passes.ColorValueOptimizer.java

Source

/*
 * 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);
    }
}