001    /*
002     * Copyright 2002 - 2007 JEuclid, http://jeuclid.sf.net
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    /* $Id: AttributesHelper.java,v 1e57f05a780b 2009/10/28 09:06:45 max $ */
018    
019    package net.sourceforge.jeuclid.elements.support.attributes;
020    
021    import java.awt.Color;
022    import java.util.HashMap;
023    import java.util.Locale;
024    import java.util.Map;
025    import java.util.StringTokenizer;
026    
027    import net.sourceforge.jeuclid.Constants;
028    import net.sourceforge.jeuclid.LayoutContext;
029    import net.sourceforge.jeuclid.elements.AbstractJEuclidElement;
030    import net.sourceforge.jeuclid.elements.support.GraphicsSupport;
031    import net.sourceforge.jeuclid.elements.support.operatordict.OperatorDictionary;
032    
033    import org.apache.commons.logging.Log;
034    import org.apache.commons.logging.LogFactory;
035    
036    /**
037     * Class contains utility methods for working with elements attributes.
038     * 
039     * @version $Revision: 1e57f05a780b $
040     */
041    public final class AttributesHelper {
042    
043        /**
044         * Constant for "Transparent" color.
045         */
046        public static final String COLOR_TRANSPARENT = "transparent";
047    
048        /**
049         * Width of veryverythinmath space according to 3.3.4.2.
050         */
051        public static final String VERYVERYTHINMATHSPACE = "0.0555556em";
052    
053        /**
054         * Width of verythinmath space according to 3.3.4.2.
055         */
056        public static final String VERYTHINMATHSPACE = "0.111111em";
057    
058        /**
059         * Width of thinmath space according to 3.3.4.2.
060         */
061        public static final String THINMATHSPACE = "0.166667em";
062    
063        /**
064         * Width of mediummath space according to 3.3.4.2.
065         */
066        public static final String MEDIUMMATHSPACE = "0.222222em";
067    
068        /**
069         * Width of tickmath space according to 3.3.4.2.
070         */
071        public static final String THICKMATHSPACE = "0.277778em";
072    
073        /**
074         * Width of verytickmath space according to 3.3.4.2.
075         */
076        public static final String VERYTHICKMATHSPACE = "0.333333em";
077    
078        /**
079         * Width of veryverytickmath space according to 3.3.4.2.
080         */
081        public static final String VERYVERYTHICKMATHSPACE = "0.388889em";
082    
083        /**
084         * Unit for pt.
085         */
086        public static final String PT = "pt";
087    
088        /**
089         * Infinity. Should be reasonably large.
090         */
091        public static final String INFINITY = "9999999pt";
092    
093        private static final String ERROR_PARSING_NUMBER = "Error Parsing number: ";
094    
095        private static final int HASHSHORT_ALPHA = 5;
096    
097        private static final int HASHSHORT_NO_ALPHA = 4;
098    
099        private static final int SHORT_INDEX_RED = 1;
100    
101        private static final int SHORT_INDEX_GREEN = 2;
102    
103        private static final int SHORT_INDEX_BLUE = 3;
104    
105        private static final int HASHLEN_ALPHA = 9;
106    
107        private static final int HASHLEN_NO_ALPHA = 7;
108    
109        private static final int HEXBASE = 16;
110    
111        private static final float MAX_HEXCHAR_AS_FLOAT = 15f;
112    
113        private static final int MAX_BYTE = 255;
114    
115        private static final float MAX_PERCENT_AS_FLOAT = 100.0f;
116    
117        private static final float MAX_BYTE_AS_FLOAT = 255f;
118    
119        private static final String PERCENT_SIGN = "%";
120    
121        private static final String COMMA = ",";
122    
123        /**
124         * Value of EM (horizontal size).
125         * <p>
126         * Please note: This is a typical value, according to
127         * http://kb.mozillazine.org/Em_vs._ex It is not dependent on the actual
128         * font used, as it should be.
129         */
130        private static final float EM = 0.83888888888888888888f;
131    
132        /**
133         * Value of EX (vertical size).
134         * <p>
135         * Please note: This is a typical value, according to
136         * http://kb.mozillazine.org/Em_vs._ex It is not dependent on the actual
137         * font used, as it should be.
138         */
139        private static final float EX = 0.5f;
140    
141        /**
142         * Default DPI value for all Java apps.
143         */
144        private static final float DPI = 72.0f;
145    
146        private static final float PERCENT = 0.01f;
147    
148        private static final float CM_PER_INCH = 2.54f;
149    
150        private static final float MM_PER_INCH = AttributesHelper.CM_PER_INCH * 10.0f;
151    
152        private static final float PT_PER_PC = 12.0f;
153    
154        private static final Map<String, String> SIZETRANSLATIONS = new HashMap<String, String>();
155    
156        private static final Map<String, Float> RELATIVE_UNITS = new HashMap<String, Float>();
157    
158        private static final Map<String, Float> ABSOLUTE_UNITS = new HashMap<String, Float>();
159    
160        private static final Map<String, Color> COLOR_MAPPINGS = new HashMap<String, Color>();
161    
162        /**
163         * Logger for this class.
164         */
165        private static final Log LOGGER = LogFactory
166                .getLog(AbstractJEuclidElement.class);
167    
168        /**
169         * Private constructor: it's forbidden to create instance of this utility
170         * class.
171         */
172        private AttributesHelper() {
173        }
174    
175        /**
176         * Parse a size that is relative to a given size.
177         * 
178         * @param sizeString
179         *            sizeString to parse
180         * @param context
181         *            Context to use to calculate for absolute sizes.
182         * @param relativeTo
183         *            This size is relative to the given size (must be in pt).
184         * @return a size in pt.
185         */
186        public static float parseRelativeSize(final String sizeString,
187                final LayoutContext context, final float relativeTo) {
188            if (sizeString == null) {
189                return relativeTo;
190            }
191            final String tSize = AttributesHelper.prepareSizeString(sizeString);
192            float retVal;
193            try {
194                if (tSize.length() >= 2) {
195                    final int valueLen = tSize.length() - 2;
196                    final String unit = tSize.substring(valueLen);
197                    final float value = Float.parseFloat(tSize.substring(0,
198                            valueLen));
199                    if (AttributesHelper.ABSOLUTE_UNITS.containsKey(unit)) {
200                        retVal = AttributesHelper.convertSizeToPt(sizeString,
201                                context, AttributesHelper.PT);
202                    } else if (AttributesHelper.RELATIVE_UNITS.containsKey(unit)) {
203                        retVal = value * relativeTo
204                                * AttributesHelper.RELATIVE_UNITS.get(unit);
205                    } else {
206                        retVal = Float.parseFloat(tSize) * relativeTo;
207                    }
208                } else {
209                    retVal = Float.parseFloat(tSize) * relativeTo;
210                }
211            } catch (final NumberFormatException nfe) {
212                retVal = relativeTo;
213                AttributesHelper.LOGGER.warn(AttributesHelper.ERROR_PARSING_NUMBER
214                        + sizeString);
215            }
216            return retVal;
217        }
218    
219        /**
220         * Translates size into pt.
221         * 
222         * @param sizeString
223         *            string to convert
224         * @param context
225         *            LayoutContext this size is relative to. This is usually the
226         *            context of the parent or the element itself.
227         * @param defaultUnit
228         *            default Unit to use in this context. May be px, pt, em, etc.
229         * @return Translated value of the size attribute into Point (=Java Pixels).
230         */
231        public static float convertSizeToPt(final String sizeString,
232                final LayoutContext context, final String defaultUnit) {
233            final String tSize = AttributesHelper.prepareSizeString(sizeString);
234            if (tSize.length() == 0) {
235                return 0;
236            }
237            float retVal;
238            try {
239                final String unit;
240                final float value;
241                if (tSize.length() <= 2) {
242                    unit = defaultUnit;
243                    value = Float.parseFloat(tSize);
244                } else {
245                    final int valueLen = tSize.length() - 2;
246                    unit = tSize.substring(valueLen);
247                    value = Float.parseFloat(tSize.substring(0, valueLen));
248                }
249                if (value == 0) {
250                    retVal = 0.0f;
251                } else if (AttributesHelper.RELATIVE_UNITS.containsKey(unit)) {
252                    retVal = value * GraphicsSupport.getFontsizeInPoint(context)
253                            * AttributesHelper.RELATIVE_UNITS.get(unit);
254                } else if (AttributesHelper.ABSOLUTE_UNITS.containsKey(unit)) {
255                    retVal = value * AttributesHelper.ABSOLUTE_UNITS.get(unit);
256                } else if (defaultUnit.length() > 0) {
257                    retVal = AttributesHelper.convertSizeToPt(sizeString
258                            + defaultUnit, context, "");
259                } else {
260                    retVal = Float.parseFloat(tSize);
261                    AttributesHelper.LOGGER.warn("Error Parsing attribute: "
262                            + sizeString + " assuming " + retVal
263                            + AttributesHelper.PT);
264                }
265            } catch (final NumberFormatException nfe) {
266                retVal = 1.0f;
267                AttributesHelper.LOGGER.warn(AttributesHelper.ERROR_PARSING_NUMBER
268                        + sizeString + " falling back to " + retVal
269                        + AttributesHelper.PT);
270            }
271            return retVal;
272        }
273    
274        private static String prepareSizeString(final String sizeString) {
275            if (sizeString == null) {
276                return "";
277            }
278            String tSize = sizeString.trim().toLowerCase(Locale.ENGLISH);
279    
280            final String translatesTo = AttributesHelper.SIZETRANSLATIONS
281                    .get(tSize);
282            if (translatesTo != null) {
283                tSize = translatesTo;
284            }
285            if (tSize.endsWith(AttributesHelper.PERCENT_SIGN)) {
286                // A nice trick because all other units are exactly 2 chars long.
287                tSize = new StringBuilder(tSize).append(' ').toString();
288            }
289            return tSize;
290        }
291    
292        /**
293         * Creates a color from a given string.
294         * <p>
295         * This function supports a wide variety of inputs.
296         * <ul>
297         * <li>#RGB (hex 0..f)</li>
298         * <li>#RGBA (hex 0..f)</li>
299         * <li>#RRGGBB (hex 00..ff)</li>
300         * <li>#RRGGBBAA (hex 00..ff)</li>
301         * <li>rgb(r,g,b) (0..255 or 0%..100%)</li>
302         * <li>java.awt.Color[r=r,g=g,b=b] (0..255)</li>
303         * <li>system-color(colorname)</li>
304         * <li>transparent</li>
305         * <li>colorname</li>
306         * </ul>
307         * 
308         * @param value
309         *            the string to parse.
310         * @return a Color representing the string if possible
311         * @param defaultValue
312         *            a default color to use in case of failure.
313         */
314        public static Color stringToColor(final String value,
315                final Color defaultValue) {
316            if (value == null) {
317                return null;
318            }
319    
320            final String lowVal = value.toLowerCase(Locale.ENGLISH);
321            Color parsedColor = null;
322    
323            if (AttributesHelper.COLOR_MAPPINGS.containsKey(lowVal)) {
324                parsedColor = AttributesHelper.COLOR_MAPPINGS.get(lowVal);
325            } else {
326                if (value.charAt(0) == '#') {
327                    parsedColor = AttributesHelper.parseWithHash(value);
328                } else if (value.startsWith("rgb(")) {
329                    parsedColor = AttributesHelper.parseAsRGB(value);
330                } else if (value.startsWith("java.awt.Color")) {
331                    parsedColor = AttributesHelper.parseAsJavaAWTColor(value);
332                }
333    
334                if (parsedColor == null) {
335                    parsedColor = defaultValue;
336                }
337    
338                if (parsedColor != null) {
339                    AttributesHelper.COLOR_MAPPINGS.put(value, parsedColor);
340                }
341            }
342            return parsedColor;
343        }
344    
345        /**
346         * Tries to parse the standard java.awt.Color toString output.
347         * 
348         * @param value
349         *            the complete line
350         * @return a color if possible
351         * @see java.awt.Color#toString()
352         */
353        private static Color parseAsJavaAWTColor(final String value) {
354            final Color parsedColor;
355            final int poss = value.indexOf('[');
356            final int pose = value.indexOf(']');
357            if ((poss == -1) || (pose == -1)) {
358                parsedColor = null;
359            } else {
360                parsedColor = AttributesHelper.parseCommaSeparatedString(value
361                        .substring(poss + 1, pose));
362            }
363            return parsedColor;
364        }
365    
366        /**
367         * Parse a color given with the rgb() function.
368         * 
369         * @param value
370         *            the complete line
371         * @return a color if possible
372         */
373        private static Color parseAsRGB(final String value) {
374            final Color parsedColor;
375            final int poss = value.indexOf('(');
376            final int pose = value.indexOf(')');
377            if ((poss == -1) || (pose == -1)) {
378                parsedColor = null;
379            } else {
380                parsedColor = AttributesHelper.parseCommaSeparatedString(value
381                        .substring(poss + 1, pose));
382            }
383            return parsedColor;
384        }
385    
386        private static Color parseCommaSeparatedString(final String value) {
387            Color parsedColor;
388            final StringTokenizer st = new StringTokenizer(value,
389                    AttributesHelper.COMMA);
390            try {
391                float red = 0.0f;
392                float green = 0.0f;
393                float blue = 0.0f;
394                if (st.hasMoreTokens()) {
395                    final String str = st.nextToken().trim();
396                    red = AttributesHelper.parseFloatOrPercent(str);
397                }
398                if (st.hasMoreTokens()) {
399                    final String str = st.nextToken().trim();
400                    green = AttributesHelper.parseFloatOrPercent(str);
401                }
402                if (st.hasMoreTokens()) {
403                    final String str = st.nextToken().trim();
404                    blue = AttributesHelper.parseFloatOrPercent(str);
405                }
406                parsedColor = new Color(red, green, blue);
407            } catch (final NumberFormatException e) {
408                AttributesHelper.LOGGER.warn(e);
409                parsedColor = null;
410            }
411            return parsedColor;
412        }
413    
414        private static float parseFloatOrPercent(final String str) {
415            final float value;
416            if (str.endsWith(AttributesHelper.PERCENT_SIGN)) {
417                value = Float.parseFloat(str.substring(0, str.length() - 1))
418                        / AttributesHelper.MAX_PERCENT_AS_FLOAT;
419            } else {
420                value = Float.parseFloat(str) / AttributesHelper.MAX_BYTE_AS_FLOAT;
421            }
422            if ((value < 0.0f) || (value > 1.0f)) {
423                throw new NumberFormatException(str + " is out of Range");
424            }
425            return value;
426        }
427    
428        /**
429         * parse a color given in the #.... format.
430         * 
431         * @param value
432         *            the complete line
433         * @return a color if possible
434         */
435        private static Color parseWithHash(final String value) {
436            Color parsedColor = null;
437            try {
438                final int len = value.length();
439                if ((len >= AttributesHelper.HASHSHORT_NO_ALPHA)
440                        && (len <= AttributesHelper.HASHSHORT_ALPHA)) {
441                    // note: divide by 15 so F = FF = 1 and so on
442                    final float red = Integer.parseInt(value.substring(
443                            AttributesHelper.SHORT_INDEX_RED,
444                            AttributesHelper.SHORT_INDEX_RED + 1),
445                            AttributesHelper.HEXBASE)
446                            / AttributesHelper.MAX_HEXCHAR_AS_FLOAT;
447                    final float green = Integer.parseInt(value.substring(
448                            AttributesHelper.SHORT_INDEX_GREEN,
449                            AttributesHelper.SHORT_INDEX_GREEN + 1),
450                            AttributesHelper.HEXBASE)
451                            / AttributesHelper.MAX_HEXCHAR_AS_FLOAT;
452                    final float blue = Integer.parseInt(value.substring(
453                            AttributesHelper.SHORT_INDEX_BLUE,
454                            AttributesHelper.SHORT_INDEX_BLUE + 1),
455                            AttributesHelper.HEXBASE)
456                            / AttributesHelper.MAX_HEXCHAR_AS_FLOAT;
457                    float alpha = 1.0f;
458                    if (len == AttributesHelper.HASHSHORT_ALPHA) {
459                        alpha = Integer.parseInt(value
460                                .substring(AttributesHelper.HASHSHORT_NO_ALPHA),
461                                AttributesHelper.HEXBASE)
462                                / AttributesHelper.MAX_HEXCHAR_AS_FLOAT;
463                    }
464                    parsedColor = new Color(red, green, blue, alpha);
465                } else if ((len == AttributesHelper.HASHLEN_NO_ALPHA)
466                        || (len == AttributesHelper.HASHLEN_ALPHA)) {
467                    final int red = Integer.parseInt(value.substring(1, 3),
468                            AttributesHelper.HEXBASE);
469                    final int green = Integer.parseInt(value.substring(3, 5),
470                            AttributesHelper.HEXBASE);
471                    final int blue = Integer.parseInt(value.substring(5,
472                            AttributesHelper.HASHLEN_NO_ALPHA),
473                            AttributesHelper.HEXBASE);
474                    int alpha = AttributesHelper.MAX_BYTE;
475                    if (len == AttributesHelper.HASHLEN_ALPHA) {
476                        alpha = Integer.parseInt(value
477                                .substring(AttributesHelper.HASHLEN_NO_ALPHA),
478                                AttributesHelper.HEXBASE);
479                    }
480                    parsedColor = new Color(red, green, blue, alpha);
481                } else {
482                    throw new NumberFormatException();
483                }
484            } catch (final NumberFormatException e) {
485                return null;
486            }
487            return parsedColor;
488        }
489    
490        /**
491         * Creates a re-parsable string representation of the given color.
492         * <p>
493         * First, the color will be converted into the sRGB colorspace. It will then
494         * be printed as #rrggbb, or as #rrrggbbaa if an alpha value is present.
495         * 
496         * @param color
497         *            the color to represent.
498         * @return a re-parsable string representadion.
499         */
500        public static String colorTOsRGBString(final Color color) {
501            if (color == null) {
502                return AttributesHelper.COLOR_TRANSPARENT;
503            }
504            final StringBuffer sbuf = new StringBuffer(10);
505            sbuf.append('#');
506            String s = Integer.toHexString(color.getRed());
507            if (s.length() == 1) {
508                sbuf.append('0');
509            }
510            sbuf.append(s);
511            s = Integer.toHexString(color.getGreen());
512            if (s.length() == 1) {
513                sbuf.append('0');
514            }
515            sbuf.append(s);
516            s = Integer.toHexString(color.getBlue());
517            if (s.length() == 1) {
518                sbuf.append('0');
519            }
520            sbuf.append(s);
521            if (color.getAlpha() != AttributesHelper.MAX_BYTE) {
522                s = Integer.toHexString(color.getAlpha());
523                if (s.length() == 1) {
524                    sbuf.append('0');
525                }
526                sbuf.append(s);
527            }
528            return sbuf.toString();
529    
530        }
531    
532        // CHECKSTYLE:OFF
533        static {
534    
535            // Mostly taken from 2.4.4.2
536            AttributesHelper.SIZETRANSLATIONS.put(
537                    OperatorDictionary.NAME_VERYVERYTHINMATHSPACE,
538                    AttributesHelper.VERYVERYTHINMATHSPACE);
539            AttributesHelper.SIZETRANSLATIONS.put(
540                    OperatorDictionary.NAME_VERYTHINMATHSPACE,
541                    AttributesHelper.VERYTHINMATHSPACE);
542            AttributesHelper.SIZETRANSLATIONS.put(
543                    OperatorDictionary.NAME_THINMATHSPACE,
544                    AttributesHelper.THINMATHSPACE);
545            AttributesHelper.SIZETRANSLATIONS.put(
546                    OperatorDictionary.NAME_MEDIUMMATHSPACE,
547                    AttributesHelper.MEDIUMMATHSPACE);
548            AttributesHelper.SIZETRANSLATIONS.put(
549                    OperatorDictionary.NAME_THICKMATHSPACE,
550                    AttributesHelper.THICKMATHSPACE);
551            AttributesHelper.SIZETRANSLATIONS.put(
552                    OperatorDictionary.NAME_VERYTHICKMATHSPACE,
553                    AttributesHelper.VERYTHICKMATHSPACE);
554            AttributesHelper.SIZETRANSLATIONS.put(
555                    OperatorDictionary.NAME_VERYVERYTHICKMATHSPACE,
556                    AttributesHelper.VERYVERYTHICKMATHSPACE);
557            AttributesHelper.SIZETRANSLATIONS.put(OperatorDictionary.NAME_INFINITY,
558                    AttributesHelper.INFINITY);
559            AttributesHelper.SIZETRANSLATIONS.put("small", "68%");
560            AttributesHelper.SIZETRANSLATIONS.put("normal", "100%");
561            AttributesHelper.SIZETRANSLATIONS.put("big", "147%");
562    
563            // For mfrac, as of 3.3.2.2
564            AttributesHelper.SIZETRANSLATIONS.put("thin", "0.5");
565            AttributesHelper.SIZETRANSLATIONS.put("medium", "1");
566            AttributesHelper.SIZETRANSLATIONS.put("thick", "2");
567    
568            AttributesHelper.SIZETRANSLATIONS.put("null", Constants.ZERO);
569    
570            AttributesHelper.RELATIVE_UNITS.put("em", AttributesHelper.EM);
571            AttributesHelper.RELATIVE_UNITS.put("ex", AttributesHelper.EX);
572            AttributesHelper.RELATIVE_UNITS.put("% ", AttributesHelper.PERCENT);
573    
574            AttributesHelper.ABSOLUTE_UNITS.put("px", 1.0f);
575            AttributesHelper.ABSOLUTE_UNITS.put("in", AttributesHelper.DPI);
576            AttributesHelper.ABSOLUTE_UNITS.put("cm", AttributesHelper.DPI
577                    / AttributesHelper.CM_PER_INCH);
578            AttributesHelper.ABSOLUTE_UNITS.put("mm", AttributesHelper.DPI
579                    / AttributesHelper.MM_PER_INCH);
580            AttributesHelper.ABSOLUTE_UNITS.put(AttributesHelper.PT, 1.0f);
581            AttributesHelper.ABSOLUTE_UNITS.put("pc", AttributesHelper.PT_PER_PC);
582    
583            // Defined in 3.2.2.2
584            AttributesHelper.COLOR_MAPPINGS.put("aqua", new Color(0, 255, 255));
585            AttributesHelper.COLOR_MAPPINGS.put("black", Color.BLACK);
586            AttributesHelper.COLOR_MAPPINGS.put("blue", Color.BLUE);
587            AttributesHelper.COLOR_MAPPINGS.put("fuchsia", new Color(255, 0, 255));
588            AttributesHelper.COLOR_MAPPINGS.put("gray", Color.GRAY);
589            AttributesHelper.COLOR_MAPPINGS.put("green", Color.GREEN);
590            AttributesHelper.COLOR_MAPPINGS.put("lime", new Color(0, 255, 0));
591            AttributesHelper.COLOR_MAPPINGS.put("maroon", new Color(128, 0, 0));
592            AttributesHelper.COLOR_MAPPINGS.put("navy", new Color(0, 0, 128));
593            AttributesHelper.COLOR_MAPPINGS.put("olive", new Color(128, 128, 0));
594            AttributesHelper.COLOR_MAPPINGS.put("purple", new Color(128, 0, 128));
595            AttributesHelper.COLOR_MAPPINGS.put("red", Color.RED);
596            AttributesHelper.COLOR_MAPPINGS.put("silver", new Color(192, 192, 192));
597            AttributesHelper.COLOR_MAPPINGS.put("teal", new Color(0, 128, 128));
598            AttributesHelper.COLOR_MAPPINGS.put("white", Color.WHITE);
599            AttributesHelper.COLOR_MAPPINGS.put("yellow", Color.YELLOW);
600    
601            // Additional colors
602            AttributesHelper.COLOR_MAPPINGS.put(AttributesHelper.COLOR_TRANSPARENT,
603                    null);
604            AttributesHelper.COLOR_MAPPINGS.put("null", null);
605            AttributesHelper.COLOR_MAPPINGS.put("", null);
606        }
607        // CHECKSTYLE:ON
608    }