com.itextpdf.tool.xml.css.CssUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xmlworker Show documentation
Show all versions of xmlworker Show documentation
Parses XML to PDF, with CSS support, using iText
/*
*
* This file is part of the iText (R) project.
Copyright (c) 1998-2019 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");
}
}