]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
Added support for the cie-lab-color() function that is found in the current XSL 2...
authorJeremias Maerki <jeremias@apache.org>
Wed, 30 Jun 2010 12:36:00 +0000 (12:36 +0000)
committerJeremias Maerki <jeremias@apache.org>
Wed, 30 Jun 2010 12:36:00 +0000 (12:36 +0000)
Some refactoring of ColorUtil to reduce code duplication.

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_Color@959286 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/fop/fo/expr/CIELabColorFunction.java [new file with mode: 0644]
src/java/org/apache/fop/fo/expr/PropertyParser.java
src/java/org/apache/fop/pdf/PDFCIELabColorSpace.java [new file with mode: 0644]
src/java/org/apache/fop/pdf/PDFColorHandler.java
src/java/org/apache/fop/pdf/PDFName.java
src/java/org/apache/fop/util/ColorUtil.java

diff --git a/src/java/org/apache/fop/fo/expr/CIELabColorFunction.java b/src/java/org/apache/fop/fo/expr/CIELabColorFunction.java
new file mode 100644 (file)
index 0000000..d027a95
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.fo.expr;
+import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.datatypes.PercentBase;
+import org.apache.fop.datatypes.PercentBaseContext;
+import org.apache.fop.fo.properties.ColorProperty;
+import org.apache.fop.fo.properties.Property;
+
+/**
+ * Implements the cie-lab-color() function.
+ * @since XSL-FO 2.0
+ */
+class CIELabColorFunction extends FunctionBase {
+
+    /**
+     * cie-lab-color() takes 2 times 3 arguments.
+     * {@inheritDoc}
+     */
+    public int nbArgs() {
+        return 2 * 3;
+    }
+
+    public PercentBase getPercentBase() {
+        return new CIELabPercentBase();
+    }
+
+    /** {@inheritDoc} */
+    public Property eval(Property[] args,
+                         PropertyInfo pInfo) throws PropertyException {
+
+        float red = args[0].getNumber().floatValue();
+        float green = args[1].getNumber().floatValue();
+        float blue = args[2].getNumber().floatValue();
+        /* Verify sRGB replacement arguments */
+        if ((red < 0 || red > 255)
+                || (green < 0 || green > 255)
+                || (blue < 0 || blue > 255)) {
+            throw new PropertyException("sRGB color values out of range. "
+                    + "Arguments to cie-lab-color() must be [0..255] or [0%..100%]");
+        }
+
+        float l = args[3].getNumber().floatValue();
+        float a = args[4].getNumber().floatValue();
+        float b = args[5].getNumber().floatValue();
+        if (l < 0 || l > 100) {
+            throw new PropertyException("L* value out of range. Valid range: [0..100]");
+        }
+        if (a < -127 || a > 127 || b < -127 || b > 127) {
+            throw new PropertyException("a* and b* values out of range. Valid range: [-127..+127]");
+        }
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("cie-lab-color(" + red + "," + green + "," + blue + ","
+                + l + "," + a + "," + b + ")");
+        FOUserAgent ua = (pInfo == null)
+                ? null
+                : (pInfo.getFO() == null ? null : pInfo.getFO().getUserAgent());
+        return ColorProperty.getInstance(ua, sb.toString());
+    }
+
+    private static class CIELabPercentBase implements PercentBase {
+        public int getDimension() {
+            return 0;
+        }
+
+        public double getBaseValue() {
+            return 1.0f;
+        }
+
+        public int getBaseLength(PercentBaseContext context) {
+            return 0;
+        }
+
+    }
+
+}
index 91218deece4f966ea924a05b088079e1e3785c9a..9ef45befe31e90b780a799a2eb61071ee1f141ca 100644 (file)
@@ -69,7 +69,8 @@ public final class PropertyParser extends PropertyTokenizer {
         FUNCTION_TABLE.put("label-end", new LabelEndFunction());
         FUNCTION_TABLE.put("body-start", new BodyStartFunction());
         FUNCTION_TABLE.put("rgb-icc", new ICCColorFunction());
-        FUNCTION_TABLE.put("rgb-named-color", new NamedColorFunction());
+        FUNCTION_TABLE.put("rgb-named-color", new NamedColorFunction()); //since XSL-FO 2.0
+        FUNCTION_TABLE.put("cie-lab-color", new CIELabColorFunction()); //since XSL-FO 2.0
         FUNCTION_TABLE.put("cmyk", new CMYKcolorFunction()); //non-standard!!!
 
         /**
diff --git a/src/java/org/apache/fop/pdf/PDFCIELabColorSpace.java b/src/java/org/apache/fop/pdf/PDFCIELabColorSpace.java
new file mode 100644 (file)
index 0000000..4d1d7f7
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.pdf;
+
+/**
+ * This class represents a "CIE L*a*b*" color space. It is expected that the components have
+ * the following ranges: L* [0..100], a* and b* [-127..127]
+ */
+public class PDFCIELabColorSpace extends PDFArray implements PDFColorSpace {
+
+    /**
+     * Creates a new "CIE L*a*b*" color space. Valid value ranges for the white and black point
+     * are [0..1] as per the PDF spec.
+     * @param whitePoint the white point
+     * @param blackPoint the optional black point (may be null)
+     */
+    public PDFCIELabColorSpace(float[] whitePoint, float[] blackPoint) {
+        super();
+
+        add(new PDFName("Lab"));
+        PDFDictionary dict = new PDFDictionary();
+        dict.put("WhitePoint", toPDFArray("White point", whitePoint));
+        if (whitePoint[1] != 1f) {
+            throw new IllegalArgumentException("The white point's Y coordinate must be 1.0");
+        }
+        if (blackPoint != null) {
+            dict.put("BlackPoint", toPDFArray("Black point", blackPoint));
+        }
+        dict.put("Range", new PDFArray(dict, new int[] {-128, 128, -128, 128}));
+        add(dict);
+    }
+
+    private PDFArray toPDFArray(String name, float[] whitePoint) {
+        PDFArray wp = new PDFArray();
+        if (whitePoint == null || whitePoint.length != 3) {
+            throw new IllegalArgumentException(name + " must be given an have 3 components");
+        }
+        for (int i = 0; i < 3; i++) {
+            wp.add(whitePoint[i]);
+        }
+        return wp;
+    }
+
+    /** {@inheritDoc} */
+    public String getName() {
+        return "CS" + this.getObjectNumber();
+    }
+
+    /** {@inheritDoc} */
+    public int getNumComponents() {
+        return 3;
+    }
+
+    /** {@inheritDoc} */
+    public boolean isCMYKColorSpace() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean isDeviceColorSpace() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean isGrayColorSpace() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean isRGBColorSpace() {
+        return false;
+    }
+
+}
index 4a5908ae1c4d8e3c7f8550f82c1e8dcd6500bfd5..3ae16b6e6ca5227d5c6e8670317c2abf9be24730 100644 (file)
@@ -24,10 +24,12 @@ import java.awt.color.ColorSpace;
 import java.awt.color.ICC_ColorSpace;
 import java.awt.color.ICC_Profile;
 import java.text.DecimalFormat;
+import java.util.Map;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.apache.xmlgraphics.java2d.color.CIELabColorSpace;
 import org.apache.xmlgraphics.java2d.color.ColorExt;
 import org.apache.xmlgraphics.java2d.color.ColorUtil;
 import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace;
@@ -46,6 +48,8 @@ public class PDFColorHandler {
 
     private PDFResources resources;
 
+    private Map cieLabColorSpaces;
+
     public PDFColorHandler(PDFResources resources) {
         this.resources = resources;
     }
@@ -73,10 +77,17 @@ public class PDFColorHandler {
                     return;
                 }
             }
+            if (log.isDebugEnabled() && alt.length > 0) {
+                log.debug("None of the alternative colors are supported. Using fallback: "
+                        + color);
+            }
         }
 
         //Fallback
-        establishColorFromColor(codeBuffer, color, fill);
+        boolean established = establishColorFromColor(codeBuffer, color, fill);
+        if (!established) {
+            establishDeviceRGB(codeBuffer, color, fill);
+        }
     }
 
     private boolean establishColorFromColor(StringBuffer codeBuffer, Color color, boolean fill) {
@@ -93,11 +104,17 @@ public class PDFColorHandler {
                 PDFSeparationColorSpace sepcs = getSeparationColorSpace((NamedColorSpace)cs);
                 establishColor(codeBuffer, sepcs, color, fill);
                 return true;
+            } else if (cs instanceof CIELabColorSpace) {
+                CIELabColorSpace labcs = (CIELabColorSpace)cs;
+                PDFCIELabColorSpace pdflab = getCIELabColorSpace(labcs);
+                selectColorSpace(codeBuffer, pdflab, fill);
+                float[] comps = color.getColorComponents(null);
+                float[] nativeComps = labcs.toNativeComponents(comps);
+                writeColor(codeBuffer, nativeComps, labcs.getNumComponents(), (fill ? "sc" : "SC"));
+                return true;
             }
         }
-        //Fallback (RGB) Color
-        establishDeviceRGB(codeBuffer, color, fill);
-        return true;
+        return false;
     }
 
     private PDFICCBasedColorSpace getICCBasedColorSpace(ICC_ColorSpace cs) {
@@ -130,15 +147,44 @@ public class PDFColorHandler {
         return sepcs;
     }
 
+    private PDFCIELabColorSpace getCIELabColorSpace(CIELabColorSpace labCS) {
+        if (this.cieLabColorSpaces == null) {
+            this.cieLabColorSpaces = new java.util.HashMap();
+        }
+        float[] wp = labCS.getWhitePoint();
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < 3; i++) {
+            if (i > 0) {
+                sb.append(',');
+            }
+            sb.append(wp[i]);
+        }
+        String key = sb.toString();
+        PDFCIELabColorSpace cielab = (PDFCIELabColorSpace)this.cieLabColorSpaces.get(key);
+        if (cielab == null) {
+            //color space is not in the PDF, yet
+            float[] wp1 = new float[] {wp[0] / 100f, wp[1] / 100f, wp[2] / 100f};
+            cielab = new PDFCIELabColorSpace(wp1, null);
+            getDocument().registerObject(cielab);
+            this.resources.addColorSpace(cielab);
+            this.cieLabColorSpaces.put(key, cielab);
+        }
+        return cielab;
+    }
+
     private void establishColor(StringBuffer codeBuffer,
             PDFColorSpace pdfcs, Color color, boolean fill) {
+        selectColorSpace(codeBuffer, pdfcs, fill);
+        writeColor(codeBuffer, color, pdfcs.getNumComponents(), (fill ? "sc" : "SC"));
+    }
+
+    private void selectColorSpace(StringBuffer codeBuffer, PDFColorSpace pdfcs, boolean fill) {
         codeBuffer.append(new PDFName(pdfcs.getName()));
         if (fill)  {
             codeBuffer.append(" cs ");
         } else {
             codeBuffer.append(" CS ");
         }
-        writeColor(codeBuffer, color, pdfcs.getNumComponents(), (fill ? "sc" : "SC"));
     }
 
     private void establishDeviceRGB(StringBuffer codeBuffer, Color color, boolean fill) {
index 42c39ef529afc3fb22b1e24d041f9ec518ad29ef..756d2fd0a49e4e40a78ea0aaf5c113ac76173f3c 100644 (file)
@@ -82,6 +82,14 @@ public class PDFName extends PDFObject {
         return this.name;
     }
 
+    /**
+     * Returns the name without the leading slash.
+     * @return the name without the leading slash
+     */
+    public String getName() {
+        return this.name.substring(1);
+    }
+
     /** {@inheritDoc} */
     public boolean equals(Object obj) {
         if (!(obj instanceof PDFName)) {
index 71c97f84e2c05178647bcd29d03b2654337f106d..775db4dcaadb90caaf0ee7a965148f1cbb6dbd86 100644 (file)
@@ -29,6 +29,7 @@ import java.util.Map;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.apache.xmlgraphics.java2d.color.CIELabColorSpace;
 import org.apache.xmlgraphics.java2d.color.ColorExt;
 import org.apache.xmlgraphics.java2d.color.ColorSpaces;
 import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace;
@@ -130,6 +131,8 @@ public final class ColorUtil {
                 parsedColor = parseAsFopRgbIcc(foUserAgent, value);
             } else if (value.startsWith("fop-rgb-named-color")) {
                 parsedColor = parseAsFopRgbNamedColor(foUserAgent, value);
+            } else if (value.startsWith("cie-lab-color")) {
+                parsedColor = parseAsCIELabColor(foUserAgent, value);
             } else if (value.startsWith("cmyk")) {
                 parsedColor = parseAsCMYK(value);
             }
@@ -234,33 +237,9 @@ public final class ColorUtil {
                     throw new PropertyException(
                             "Invalid number of arguments: rgb(" + value + ")");
                 }
-                float red = 0.0f, green = 0.0f, blue = 0.0f;
-                String str = args[0].trim();
-                if (str.endsWith("%")) {
-                    red = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100f;
-                } else {
-                    red = Float.parseFloat(str) / 255f;
-                }
-                str = args[1].trim();
-                if (str.endsWith("%")) {
-                    green = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100f;
-                } else {
-                    green = Float.parseFloat(str) / 255f;
-                }
-                str = args[2].trim();
-                if (str.endsWith("%")) {
-                    blue = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100f;
-                } else {
-                    blue = Float.parseFloat(str) / 255f;
-                }
-                if ((red < 0.0 || red > 1.0)
-                        || (green < 0.0 || green > 1.0)
-                        || (blue < 0.0 || blue > 1.0)) {
-                    throw new PropertyException("Color values out of range");
-                }
+                float red = parseComponent255(args[0], value);
+                float green = parseComponent255(args[1], value);
+                float blue = parseComponent255(args[2], value);
                 parsedColor = new ColorExt(red, green, blue, null);
             } catch (PropertyException pe) {
                 //simply re-throw
@@ -276,6 +255,38 @@ public final class ColorUtil {
         return parsedColor;
     }
 
+    private static float parseComponent255(String str, String function)
+            throws PropertyException {
+        float component;
+        str = str.trim();
+        if (str.endsWith("%")) {
+            component = Float.parseFloat(str.substring(0,
+                    str.length() - 1)) / 100f;
+        } else {
+            component = Float.parseFloat(str) / 255f;
+        }
+        if ((component < 0.0 || component > 1.0)) {
+            throw new PropertyException("Color value out of range for " + function + ": "
+                    + str + ". Valid range: [0..255] or [0%..100%]");
+        }
+        return component;
+    }
+
+    private static float parseComponent1(String argument, String function)
+            throws PropertyException {
+        return parseComponent(argument, 0f, 1f, function);
+    }
+
+    private static float parseComponent(String argument, float min, float max, String function)
+            throws PropertyException {
+        float component = Float.parseFloat(argument.trim());
+        if ((component < min || component > max)) {
+            throw new PropertyException("Color value out of range for " + function + ": "
+                    + argument + ". Valid range: [" + min + ".." + max + "]");
+        }
+        return component;
+    }
+
     /**
      * parse a color given in the #.... format.
      *
@@ -338,17 +349,9 @@ public final class ColorUtil {
                 }
 
                 //Set up fallback sRGB value
-                float red = 0, green = 0, blue = 0;
-                red = Float.parseFloat(args[0].trim());
-                green = Float.parseFloat(args[1].trim());
-                blue = Float.parseFloat(args[2].trim());
-                /* Verify rgb replacement arguments */
-                if ((red < 0 || red > 1)
-                        || (green < 0 || green > 1)
-                        || (blue < 0 || blue > 1)) {
-                    throw new PropertyException("Color values out of range. "
-                            + "Fallback RGB arguments to fop-rgb-icc() must be [0..1]");
-                }
+                float red = parseComponent1(args[0], value);
+                float green = parseComponent1(args[1], value);
+                float blue = parseComponent1(args[2], value);
                 Color sRGB = new ColorExt(red, green, blue, null);
 
                 /* Get and verify ICC profile name */
@@ -435,20 +438,13 @@ public final class ColorUtil {
 
             try {
                 if (args.length != 6) {
-                    throw new PropertyException("rgb-named() function must have 6 arguments");
+                    throw new PropertyException("rgb-named-color() function must have 6 arguments");
                 }
 
                 //Set up fallback sRGB value
-                float red = Float.parseFloat(args[0].trim());
-                float green = Float.parseFloat(args[1].trim());
-                float blue = Float.parseFloat(args[2].trim());
-                /* Verify rgb replacement arguments */
-                if ((red < 0 || red > 1)
-                        || (green < 0 || green > 1)
-                        || (blue < 0 || blue > 1)) {
-                    throw new PropertyException("Color values out of range. "
-                            + "Fallback RGB arguments to fop-rgb-named-color() must be [0..1]");
-                }
+                float red = parseComponent1(args[0], value);
+                float green = parseComponent1(args[1], value);
+                float blue = parseComponent1(args[2], value);
                 Color sRGB = new ColorExt(red, green, blue, null);
 
                 /* Get and verify ICC profile name */
@@ -518,6 +514,55 @@ public final class ColorUtil {
         return parsedColor;
     }
 
+    /**
+     * Parse a color specified using the cie-lab-color() function.
+     *
+     * @param value the function call
+     * @return a color if possible
+     * @throws PropertyException if the format is wrong.
+     */
+    private static Color parseAsCIELabColor(FOUserAgent foUserAgent, String value)
+            throws PropertyException {
+        Color parsedColor;
+        int poss = value.indexOf("(");
+        int pose = value.indexOf(")");
+        if (poss != -1 && pose != -1) {
+            String[] args = value.substring(poss + 1, pose).split(",");
+
+            try {
+                if (args.length != 6) {
+                    throw new PropertyException("cie-lab-color() function must have 6 arguments");
+                }
+
+                //Set up fallback sRGB value
+                float red = parseComponent255(args[0], value);
+                float green = parseComponent255(args[1], value);
+                float blue = parseComponent255(args[2], value);
+
+                float l = parseComponent(args[3], 0f, 100f, value);
+                float a = parseComponent(args[4], -127f, 127f, value);
+                float b = parseComponent(args[5], -127f, 127f, value);
+
+                //Assuming the XSL-FO spec uses the D50 white point
+                CIELabColorSpace cs = ColorSpaces.getCIELabColorSpaceD50();
+                //use toColor() to have components normalized
+                Color labColor = cs.toColor(l, a, b, 1.0f);
+                parsedColor = new ColorExt(red, green, blue, new Color[] {labColor});
+
+            } catch (PropertyException pe) {
+                //simply re-throw
+                throw pe;
+            } catch (Exception e) {
+                //wrap in a PropertyException
+                throw new PropertyException(e);
+            }
+        } else {
+            throw new PropertyException("Unknown color format: " + value
+                    + ". Must be cie-lab-color(r,g,b,Lightness,a-value,b-value)");
+        }
+        return parsedColor;
+    }
+
     private static String unescapeString(String iccProfileSrc) {
         if (iccProfileSrc.startsWith("\"") || iccProfileSrc.startsWith("'")) {
             iccProfileSrc = iccProfileSrc.substring(1);
@@ -549,43 +594,10 @@ public final class ColorUtil {
                     throw new PropertyException(
                             "Invalid number of arguments: cmyk(" + value + ")");
                 }
-                float cyan = 0.0f, magenta = 0.0f, yellow = 0.0f, black = 0.0f;
-                String str = args[0].trim();
-                if (str.endsWith("%")) {
-                  cyan  = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100.0f;
-                } else {
-                  cyan  = Float.parseFloat(str);
-                }
-                str = args[1].trim();
-                if (str.endsWith("%")) {
-                  magenta = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100.0f;
-                } else {
-                  magenta = Float.parseFloat(str);
-                }
-                str = args[2].trim();
-                if (str.endsWith("%")) {
-                  yellow = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100.0f;
-                } else {
-                  yellow = Float.parseFloat(str);
-                }
-                str = args[3].trim();
-                if (str.endsWith("%")) {
-                  black = Float.parseFloat(str.substring(0,
-                            str.length() - 1)) / 100.0f;
-                } else {
-                  black = Float.parseFloat(str);
-                }
-
-                if ((cyan < 0.0 || cyan > 1.0)
-                        || (magenta < 0.0 || magenta > 1.0)
-                        || (yellow < 0.0 || yellow > 1.0)
-                        || (black < 0.0 || black > 1.0)) {
-                    throw new PropertyException("Color values out of range"
-                            + "Arguments to cmyk() must be in the range [0%-100%] or [0.0-1.0]");
-                }
+                float cyan = parseComponent1(args[0], value);
+                float magenta = parseComponent1(args[1], value);
+                float yellow = parseComponent1(args[2], value);
+                float black = parseComponent1(args[3], value);
                 float[] comps = new float[] {cyan, magenta, yellow, black};
                 parsedColor = DeviceCMYKColorSpace.createColorExt(comps);
             } catch (PropertyException pe) {
@@ -671,6 +683,9 @@ public final class ColorUtil {
         if (icc == null) {
             return toRGBFunctionCall(color);
         }
+        if (icc.getColorSpace() instanceof CIELabColorSpace) {
+            return toCIELabFunctionCall(color, icc);
+        }
         StringBuffer sb = new StringBuffer(40);
 
         String functionName;
@@ -706,6 +721,20 @@ public final class ColorUtil {
         return functionName + sb.toString();
     }
 
+    private static String toCIELabFunctionCall(ColorExt color, Color cieLab) {
+        StringBuffer sb = new StringBuffer("cie-lab-color(");
+        sb.append(color.getRed()).append(',');
+        sb.append(color.getGreen()).append(',');
+        sb.append(color.getBlue());
+        CIELabColorSpace cs = (CIELabColorSpace)cieLab.getColorSpace();
+        float[] lab = cs.toNativeComponents(cieLab.getColorComponents(null));
+        for (int i = 0; i < 3; i++) {
+            sb.append(',').append(lab[i]);
+        }
+        sb.append(')');
+        return sb.toString();
+    }
+
     private static Color createColor(int r, int g, int b) {
         return new ColorExt(r, g, b, null);
     }