
com.itextpdf.tool.xml.css.CssUtils Maven / Gradle / Ivy
/*
*
* This file is part of the iText (R) project.
Copyright (c) 1998-2022 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 - 2025 Weber Informatics LLC | Privacy Policy