All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.tool.xml.css.CssUtils Maven / Gradle / Ivy

There is a newer version: 5.5.13.4
Show newest version
/*
 *
 * This file is part of the iText (R) project.
    Copyright (c) 1998-2020 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: [email protected]
 */
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. Note: Does not work when double
	 * spaces are in the boxes value. (Tip: 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 parseBoxValues(final java.lang.String box,
                                             final java.lang.String pre, final java.lang.String post) {

        return parseBoxValues(box, pre, post, null);
    }
    public Map parseBoxValues(final String box,
                                              final String pre, final String post, String preKey) {
		String[] props = box.split(" ");
		int length = props.length;
		Map map = new LinkedHashMap(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 borderwidth = new HashSet(
			Arrays.asList(new String[] { CSS.Value.THIN, CSS.Value.MEDIUM, CSS.Value.THICK })); //  thin = 1px, medium = 3px, thick = 5px
	private static final Set borderstyle = new HashSet(
			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 parseBorder(final String border) {
        return parseBorder(border, null);
    }
	public Map parseBorder(final String border, final String borderKey) {
		HashMap map = new HashMap(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 backgroundPositions = new HashSet(
			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 processBackground(final String background) {
		Map rules = new HashMap();
		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 processListStyle(final String listStyle) {
		Map rules = new HashMap();
		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 processFont(final String font) {
		Map rules = new HashMap();
		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)}).
* 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)}).
* 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 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:
*
    *
  • a numeric value in pixels (e.g. 123, 1.23, .123),
  • *
  • a value with a metric unit (px, in, cm, mm, pc or pt) attached to it,
  • *
  • or a value with a relative value (%, em, ex).
  • *
* Note: baseValue must be in pt.

* @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.
* (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.
* 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.
* 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).

* Determines the position between digits and affiliated characters ('+','-','0-9' and '.') and all other characters.
* 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 url("file.jpg") to file.jpg. * @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 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. *

* 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. *

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy