/* * * This file is part of the iText (R) project. Copyright (c) 1998-2017 iText Group NV * Authors: Balder Van Camp, Emiel Ackermann, et al. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA, 02110-1301 USA, or download the license from the following URL: * http://itextpdf.com/terms-of-use/ * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * In accordance with Section 7(b) of the GNU Affero General Public License, * a covered work must retain the producer line in every PDF that is created * or manipulated using iText. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the iText software without * disclosing the source code of your own applications. * These activities include: offering paid services to customers as an ASP, * serving PDFs on the fly in a web application, shipping iText with a closed * source product. * * For more information, please contact iText Software Corp. at this * address: sales@itextpdf.com */ package com.itextpdf.tool.xml.css; import com.itextpdf.text.html.HtmlTags; import com.itextpdf.text.html.WebColors; import com.itextpdf.tool.xml.Tag; import com.itextpdf.tool.xml.css.apply.MarginMemory; import com.itextpdf.tool.xml.exceptions.NoDataException; import java.text.MessageFormat; import java.util.*; /** * @author redlab_b * */ public class CssUtils { private static final String COLOR = "-color"; private static final String STYLE = "-style"; private static final String WIDTH = "-width"; private static final String BORDER2 = "border-"; private static final String _0_LEFT_1 = "{0}left{1}"; private static final String _0_RIGHT_1 = "{0}right{1}"; private static final String _0_BOTTOM_1 = "{0}bottom{1}"; private static final String _0_TOP_1 = "{0}top{1}"; private static CssUtils instance = new CssUtils(); /** * Default font size if none is set. */ public static final int DEFAULT_FONT_SIZE_PT = 12; /** * @return Singleton instance of CssUtils. */ public static CssUtils getInstance() { return instance; } /** * */ private CssUtils() { } /** * Returns the top, bottom, left, right version for the given box. the keys * will be the pre value concatenated with either top, bottom, right or left * and the post value. <strong>Note:</strong> Does not work when double * spaces are in the boxes value. (<strong>Tip:</strong> Use * {@link CssUtils#stripDoubleSpacesAndTrim(String)}) * * @param box * the value to parse * @param pre * the pre key part * @param post * the post key part * @return a map with the parsed properties */ public Map<String, String> parseBoxValues(final java.lang.String box, final java.lang.String pre, final java.lang.String post) { return parseBoxValues(box, pre, post, null); } public Map<String, String> parseBoxValues(final String box, final String pre, final String post, String preKey) { String[] props = box.split(" "); int length = props.length; Map<String, String> map = new LinkedHashMap<String, String>(4); if (length == 1) { String value = props[0]; if (preKey == null) { map.put(MessageFormat.format(_0_TOP_1, pre, post), value); map.put(MessageFormat.format(_0_BOTTOM_1, pre, post), value); map.put(MessageFormat.format(_0_RIGHT_1, pre, post), value); map.put(MessageFormat.format(_0_LEFT_1, pre, post), value); } else { map.put(MessageFormat.format(preKey + "{0}", post), value); } } else if (length == 2) { if (preKey == null) { map.put(MessageFormat.format(_0_TOP_1, pre, post), props[0]); map.put(MessageFormat.format(_0_BOTTOM_1, pre, post), props[0]); map.put(MessageFormat.format(_0_RIGHT_1, pre, post), props[1]); map.put(MessageFormat.format(_0_LEFT_1, pre, post), props[1]); } else { map.put(MessageFormat.format(preKey + "{0}", post), props[0]); } } else if (length == 3) { if (preKey == null) { map.put(MessageFormat.format(_0_TOP_1, pre, post), props[0]); map.put(MessageFormat.format(_0_BOTTOM_1, pre, post), props[2]); map.put(MessageFormat.format(_0_RIGHT_1, pre, post), props[1]); map.put(MessageFormat.format(_0_LEFT_1, pre, post), props[1]); } else { map.put(MessageFormat.format(preKey + "{0}", post), props[0]); } } else if (length == 4) { if (preKey == null) { map.put(MessageFormat.format(_0_TOP_1, pre, post), props[0]); map.put(MessageFormat.format(_0_BOTTOM_1, pre, post), props[2]); map.put(MessageFormat.format(_0_RIGHT_1, pre, post), props[1]); map.put(MessageFormat.format(_0_LEFT_1, pre, post), props[3]); } else { map.put(MessageFormat.format(preKey + "{0}", post), props[0]); } } return map; } private static final Set<String> borderwidth = new HashSet<String>( Arrays.asList(new String[] { CSS.Value.THIN, CSS.Value.MEDIUM, CSS.Value.THICK })); // thin = 1px, medium = 3px, thick = 5px private static final Set<String> borderstyle = new HashSet<String>( Arrays.asList(new String[] { CSS.Value.NONE, CSS.Value.HIDDEN, CSS.Value.DOTTED, CSS.Value.DASHED, CSS.Value.SOLID, CSS.Value.DOUBLE, CSS.Value.GROOVE, CSS.Value.RIDGE, CSS.Value.INSET, CSS.Value.OUTSET})); /** * @param border * the border property * @return a map of the border property parsed to each property (width, * style, color). */ public Map<String, String> parseBorder(final String border) { return parseBorder(border, null); } public Map<String, String> parseBorder(final String border, final String borderKey) { HashMap<String, String> map = new HashMap<String, String>(0); String split[] = splitComplexCssStyle(border); int length = split.length; if (length == 1) { if (borderwidth.contains(split[0]) || isNumericValue(split[0]) || isMetricValue(split[0])) { map.putAll(parseBoxValues(split[0], BORDER2, WIDTH, borderKey)); } else { map.putAll(parseBoxValues(split[0], BORDER2, STYLE, borderKey)); } } else { for(int i = 0 ; i<length ; i++) { String value = split[i]; if (borderwidth.contains(value) || isNumericValue(value) || isMetricValue(value)) { map.putAll(parseBoxValues(value, BORDER2, WIDTH, borderKey)); } else if(borderstyle.contains(value)){ map.putAll(parseBoxValues(value, BORDER2, STYLE, borderKey)); } else if(value.contains("rgb(") || value.contains("#") || WebColors.NAMES.containsKey(value.toLowerCase())){ map.putAll(parseBoxValues(value, BORDER2, COLOR, borderKey)); } } } return map; } /** * Trims and Strips double spaces from the given string. * * @param str * the string to strip * @return the string without double spaces */ public String stripDoubleSpacesAndTrim(final String str) { char[] charArray = str.toCharArray(); if (str.contains(" ")) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < charArray.length; i++) { char c = charArray[i]; if (c != ' ') { builder.append(c); } else { if (i + 1 < charArray.length && charArray[i + 1] != ' ') { builder.append(' '); } } } return builder.toString().trim(); } else { return String.valueOf(charArray).trim(); } } public String stripDoubleSpacesTrimAndToLowerCase(final String str) { return stripDoubleSpacesAndTrim(str).toLowerCase(); } private static final Set<String> backgroundPositions = new HashSet<String>( Arrays.asList(new String[] { CSS.Value.LEFT, CSS.Value.CENTER, CSS.Value.BOTTOM, CSS.Value.TOP, CSS.Value.RIGHT })); /** * Preparation before implementing the background style in iText. Splits the * given background style and its attributes into background-color, * background-image, background-repeat, background-attachment, * background-position and css styles. * * @param background * the string containing the font style value. * @return a map with the values of font parsed into each css property. */ public Map<String, String> processBackground(final String background) { Map<String, String> rules = new HashMap<String, String>(); String[] styles = splitComplexCssStyle(background); for(String style : styles) { if (style.contains("url(")) { rules.put(CSS.Property.BACKGROUND_IMAGE, style); } else if (style.equalsIgnoreCase(CSS.Value.REPEAT) || style.equalsIgnoreCase(CSS.Value.NO_REPEAT) || style.equalsIgnoreCase(CSS.Value.REPEAT_X) || style.equalsIgnoreCase(CSS.Value.REPEAT_Y)) { rules.put(CSS.Property.BACKGROUND_REPEAT, style); } else if (style.equalsIgnoreCase(CSS.Value.FIXED) || style.equalsIgnoreCase(CSS.Value.SCROLL)) { rules.put(CSS.Property.BACKGROUND_ATTACHMENT, style); } else if (backgroundPositions.contains(style)) { if(rules.get(CSS.Property.BACKGROUND_POSITION) == null) { rules.put(CSS.Property.BACKGROUND_POSITION, style); } else { style = style.concat(" "+rules.get(CSS.Property.BACKGROUND_POSITION)); rules.put(CSS.Property.BACKGROUND_POSITION, style); } } else if (isNumericValue(style) || isMetricValue(style) || isRelativeValue(style)) { if(rules.get(CSS.Property.BACKGROUND_POSITION) == null) { rules.put(CSS.Property.BACKGROUND_POSITION, style); } else { style = style.concat(" "+rules.get(CSS.Property.BACKGROUND_POSITION)); rules.put(CSS.Property.BACKGROUND_POSITION, style); } } else if(style.contains("rgb(") || style.contains("rgba(") || style.contains("#") || WebColors.NAMES.containsKey(style.toLowerCase())) { rules.put(CSS.Property.BACKGROUND_COLOR, style); } } return rules; } /** * Preparation before implementing the list style in iText. Splits the given * list style and its attributes into list-style-type, list-style-position and list-style-image. * * @param listStyle the string containing the list style value. * @return a map with the values of the parsed list style into each css property. */ public Map<String, String> processListStyle(final String listStyle) { Map<String, String> rules = new HashMap<String, String>(); String[] styles = splitComplexCssStyle(listStyle); for(String style : styles) { if (style.equalsIgnoreCase(CSS.Value.DISC) || style.equalsIgnoreCase(CSS.Value.SQUARE) || style.equalsIgnoreCase(CSS.Value.CIRCLE) || style.equalsIgnoreCase(CSS.Value.LOWER_ROMAN) || style.equalsIgnoreCase(CSS.Value.UPPER_ROMAN) || style.equalsIgnoreCase(CSS.Value.LOWER_GREEK) || style.equalsIgnoreCase(CSS.Value.UPPER_GREEK) || style.equalsIgnoreCase(CSS.Value.LOWER_ALPHA) || style.equalsIgnoreCase(CSS.Value.UPPER_ALPHA) || style.equalsIgnoreCase(CSS.Value.LOWER_LATIN) || style.equalsIgnoreCase(CSS.Value.UPPER_LATIN)) { rules.put(CSS.Property.LIST_STYLE_TYPE, style); } else if (style.equalsIgnoreCase(CSS.Value.INSIDE) || style.equalsIgnoreCase(CSS.Value.OUTSIDE)) { rules.put(CSS.Property.LIST_STYLE_POSITION, style); } else if (style.contains("url(")) { rules.put(CSS.Property.LIST_STYLE_IMAGE, style); } } return rules; } /** * Preparation before implementing the font style in iText. Splits the given * font style and its attributes into font-size, line-height, * font-weight, font-style, font-variant and font-family css styles. * * @param font the string containing the font style value. * @return a map with the values of the parsed font into each css property. */ public Map<String, String> processFont(final String font) { Map<String, String> rules = new HashMap<String, String>(); String[] styleAndRest = font.split("\\s"); for (int i = 0 ; i < styleAndRest.length ; i++){ String style = styleAndRest[i]; if (style.equalsIgnoreCase(HtmlTags.ITALIC) || style.equalsIgnoreCase(HtmlTags.OBLIQUE)) { rules.put(HtmlTags.FONTSTYLE, style); } else if (style.equalsIgnoreCase("small-caps")){ rules.put("font-variant", style); } else if (style.equalsIgnoreCase(HtmlTags.BOLD)){ rules.put(HtmlTags.FONTWEIGHT, style); } else if (isMetricValue(style) || isNumericValue(style)){ if (style.contains("/")) { String[] sizeAndLineHeight = style.split("/"); style = sizeAndLineHeight[0]; // assuming font-size always is the first parameter rules.put(HtmlTags.LINEHEIGHT, sizeAndLineHeight[1]); } rules.put(HtmlTags.FONTSIZE, style); if (i != styleAndRest.length-1){ String rest = styleAndRest[i+1]; rest = rest.replaceAll("\"", ""); rest = rest.replaceAll("'", ""); rules.put(HtmlTags.FONTFAMILY, rest); } } } return rules; } /** * Use only if value of style is a metric value ({@link CssUtils#isMetricValue(String)}) or a numeric value in pixels ({@link CssUtils#isNumericValue(String)}).<br /> * Checks if the style is present in the css of the tag, then parses it to pt. and returns the parsed value. * @param t the tag which needs to be checked. * @param style the style which needs to be checked. * @return float the parsed value of the style or 0f if the value was invalid. */ public float checkMetricStyle(final Tag t, final String style) { Float metricValue = checkMetricStyle(t.getCSS(), style); if (metricValue != null) { return metricValue; } return 0f; } /** * Use only if value of style is a metric value ({@link CssUtils#isMetricValue(String)}) or a numeric value in pixels ({@link CssUtils#isNumericValue(String)}).<br /> * Checks if the style is present in the css of the tag, then parses it to pt. and returns the parsed value. * @param css the map of css styles which needs to be checked. * @param style the style which needs to be checked. * @return float the parsed value of the style or 0f if the value was invalid. */ public Float checkMetricStyle(final Map<String,String> css, final String style) { String value = css.get(style); if (value != null && (isMetricValue(value) || isNumericValue(value))) { return parsePxInCmMmPcToPt(value); } return null; } /** * Checks whether a string contains an allowed metric unit in HTML/CSS; px, in, cm, mm, pc or pt. * @param value the string that needs to be checked. * @return boolean true if value contains an allowed metric value. */ public boolean isMetricValue(final String value) { return value.contains(CSS.Value.PX) || value.contains(CSS.Value.IN) || value.contains(CSS.Value.CM) || value.contains(CSS.Value.MM) || value.contains(CSS.Value.PC) || value.contains(CSS.Value.PT); } /** * Checks whether a string contains an allowed value relative to previously set value. * @param value the string that needs to be checked. * @return boolean true if value contains an allowed metric value. */ public boolean isRelativeValue(final String value) { return value.contains(CSS.Value.PERCENTAGE) || value.contains(CSS.Value.EM) || value.contains(CSS.Value.EX); } /** * Checks whether a string matches a numeric value (e.g. 123, 1.23, .123). All these metric values are allowed in HTML/CSS. * @param value the string that needs to be checked. * @return boolean true if value contains an allowed metric value. */ public boolean isNumericValue(final String value) { return value.matches("^-?\\d\\d*\\.\\d*$") || value.matches("^-?\\d\\d*$") || value.matches("^-?\\.\\d\\d*$"); } /** * Convenience method for parsing a value to pt if a value can contain: <br /> * <ul> * <li>a numeric value in pixels (e.g. 123, 1.23, .123),</li> * <li>a value with a metric unit (px, in, cm, mm, pc or pt) attached to it,</li> * <li>or a value with a relative value (%, em, ex).</li> * </ul> * <b>Note:</b> baseValue must be in pt.<br /><br /> * @param value the string containing the value to be parsed. * @param baseValue float needed for the calculation of the relative value. * @return parsedValue float containing the parsed value in pt. */ public float parseValueToPt(final String value, final float baseValue) { float parsedValue = 0; if(isMetricValue(value) || isNumericValue(value)) { parsedValue = parsePxInCmMmPcToPt(value); } else if (isRelativeValue(value)) { parsedValue = parseRelativeValue(value, baseValue); } return parsedValue; } /** * Parses an relative value based on the base value that was given, in the metric unit of the base value. <br /> * (e.g. margin=10% should be based on the page width, so if an A4 is used, the margin = 0.10*595.0 = 59.5f) * @param relativeValue in %, em or ex. * @param baseValue the value the returned float is based on. * @return the parsed float in the metric unit of the base value. */ public float parseRelativeValue(final String relativeValue, final float baseValue) { int pos = determinePositionBetweenValueAndUnit(relativeValue); if (pos == 0) return 0f; float f = Float.parseFloat(relativeValue.substring(0, pos) + "f"); String unit = relativeValue.substring(pos); if (unit.startsWith("%")) { f = baseValue * f / 100; } else if (unit.startsWith("em")) { f = baseValue * f; } else if (unit.contains("ex")) { f = baseValue * f / 2; } return f; } /** * Parses a length with an allowed metric unit (px, pt, in, cm, mm, pc, em or ex) or numeric value (e.g. 123, 1.23, * .123) to pt.<br /> * A numeric value (without px, pt, etc in the given length string) is considered to be in the default metric that * was given. * * @param length the string containing the length. * @param defaultMetric the string containing the metric if it is possible that the length string does not contain * one. If null the length is considered to be in px as is default in HTML/CSS. * @return parsed value */ public float parsePxInCmMmPcToPt(final String length, final String defaultMetric) { int pos = determinePositionBetweenValueAndUnit(length); if (pos == 0) return 0f; float f = Float.parseFloat(length.substring(0, pos) + "f"); String unit = length.substring(pos); // inches if (unit.startsWith(CSS.Value.IN) || (unit.equals("") && defaultMetric.equals(CSS.Value.IN))) { f *= 72f; } // centimeters else if (unit.startsWith(CSS.Value.CM) || (unit.equals("") && defaultMetric.equals(CSS.Value.CM))) { f = (f / 2.54f) * 72f; } // millimeters else if (unit.startsWith(CSS.Value.MM) || (unit.equals("") && defaultMetric.equals(CSS.Value.MM))) { f = (f / 25.4f) * 72f; } // picas else if (unit.startsWith(CSS.Value.PC) || (unit.equals("") && defaultMetric.equals(CSS.Value.PC))) { f *= 12f; } // pixels (1px = 0.75pt). else if (unit.startsWith(CSS.Value.PX) || (unit.equals("") && defaultMetric.equals(CSS.Value.PX))) { f *= 0.75f; } return f; } /** * Parses a length with an allowed metric unit (px, pt, in, cm, mm, pc, em or ex) or numeric value (e.g. 123, 1.23, .123) to pt.<br /> * A numeric value is considered to be in px as is default in HTML/CSS. * @param length the string containing the length. * @return float the parsed length in pt. */ public float parsePxInCmMmPcToPt(final String length) { return parsePxInCmMmPcToPt(length, CSS.Value.PX); } /** * Method used in preparation of splitting a string containing a numeric value with a metric unit (e.g. 18px, 9pt, 6cm, etc).<br /><br /> * Determines the position between digits and affiliated characters ('+','-','0-9' and '.') and all other characters.<br /> * e.g. string "16px" will return 2, string "0.5em" will return 3 and string '-8.5mm' will return 4. * * @param string containing a numeric value with a metric unit * @return int position between the numeric value and unit or 0 if string is null or string started with a non-numeric value. */ public int determinePositionBetweenValueAndUnit(final String string) { if (string == null) return 0; int pos = 0; boolean ok = true; while (ok && pos < string.length()) { switch (string.charAt(pos)) { case '+': case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': pos++; break; default: ok = false; } } return pos; } /** * Returns the sum of the left and right margin of a tag. * @param t the tag of which the total horizontal margin is needed. * @param pageWidth the page width * @return float the total horizontal margin. */ public float getLeftAndRightMargin(final Tag t, final float pageWidth) { float horizontalMargin = 0; String value = t.getCSS().get(CSS.Property.MARGIN_LEFT); if (value != null) { horizontalMargin += parseValueToPt(value, pageWidth); } value = t.getCSS().get(CSS.Property.MARGIN_RIGHT); if (value != null) { horizontalMargin += parseValueToPt(value, pageWidth); } return horizontalMargin; } /** * Parses <code>url("file.jpg")</code> to <code>file.jpg</code>. * @param url the url attribute to parse * @return the parsed url. Or original url if not wrappend in url() */ public String extractUrl(final String url) { String str = null; if (url.startsWith("url")) { String urlString = url.substring(3).trim().replace("(", "").replace(")", "").trim(); if (urlString.startsWith("'") && urlString.endsWith("'")) { str = urlString.substring(urlString.indexOf("'")+1, urlString.lastIndexOf("'")); } else if ( urlString.startsWith("\"") && urlString.endsWith("\"") ) { str = urlString.substring(urlString.indexOf('"')+1, urlString.lastIndexOf('"')); } else { str = urlString; } } else { // assume it's an url without wrapping in "url()" str = url; } return str; } /** * Validates a given textHeight based on the content of a tag against the css styles "min-height" and "max-height" of the tag if present. * * @param css the styles of a tag * @param textHeight the current textHeight based on the content of a tag * @return the final text height of an element. */ public float validateTextHeight(final Map<String, String> css, float textHeight) { if(null != css.get("min-height") && textHeight < new CssUtils().parsePxInCmMmPcToPt(css.get("min-height"))) { textHeight = new CssUtils().parsePxInCmMmPcToPt(css.get("min-height")); } else if(null != css.get("max-height") && textHeight > new CssUtils().parsePxInCmMmPcToPt(css.get("max-height"))) { textHeight = new CssUtils().parsePxInCmMmPcToPt(css.get("max-height")); } return textHeight; } /** * Calculates the margin top or spacingBefore based on the given value and the last margin bottom. * <br /><br /> * In HTML the margin-bottom of a tag overlaps with the margin-top of a following tag. * This method simulates this behavior by subtracting the margin-top value of the given tag from the margin-bottom of the previous tag. The remaining value is returned or if the margin-bottom value is the largest, 0 is returned * @param value the margin-top value of the given tag. * @param largestFont used if a relative value was given to calculate margin. * @param configuration XmlWorkerConfig containing the last margin bottom. * @return an offset */ public float calculateMarginTop(final String value, final float largestFont, final MarginMemory configuration) { return calculateMarginTop(parseValueToPt(value, largestFont), configuration); } /** * Calculates the margin top or spacingBefore based on the given value and the last margin bottom. * <br /><br /> * In HTML the margin-bottom of a tag overlaps with the margin-top of a following tag. * This method simulates this behavior by subtracting the margin-top value of the given tag from the margin-bottom of the previous tag. The remaining value is returned or if the margin-bottom value is the largest, 0 is returned * @param value float containing the margin-top value. * @param configuration XmlWorkerConfig containing the last margin bottom. * @return an offset */ public float calculateMarginTop(final float value, final MarginMemory configuration) { float marginTop = value; try { float marginBottom = configuration.getLastMarginBottom(); marginTop = (marginTop>marginBottom)?marginTop-marginBottom:0; } catch (NoDataException e) { } return marginTop; } /** * Trims a string and removes surrounding " or '. * * @param s the string * @return trimmed and unquoted string */ public String trimAndRemoveQuoutes(String s) { s = s.trim(); if ((s.startsWith("\"") || s.startsWith("'")) && s.endsWith("\"") || s.endsWith("'")) { s = s.substring(1, s.length() - 1); } return s; } public String[] splitComplexCssStyle(String s) { s = s.replaceAll("\\s*,\\s*", ",") ; return s.split("\\s"); } }