com.itextpdf.html2pdf.css.resolve.DefaultCssResolver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of html2pdf Show documentation
Show all versions of html2pdf Show documentation
pdfHTML is an iText add-on that lets you to parse (X)HTML snippets and the associated CSS and converts
them to PDF.
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2020 iText Group NV
Authors: Bruno Lowagie, Paulo Soares, 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.html2pdf.css.resolve;
import com.itextpdf.html2pdf.LogMessageConstant;
import com.itextpdf.html2pdf.attach.ProcessorContext;
import com.itextpdf.html2pdf.css.CssConstants;
import com.itextpdf.html2pdf.css.apply.util.CounterProcessorUtil;
import com.itextpdf.html2pdf.exception.Html2PdfException;
import com.itextpdf.html2pdf.html.AttributeConstants;
import com.itextpdf.html2pdf.html.HtmlUtils;
import com.itextpdf.html2pdf.html.TagConstants;
import com.itextpdf.io.util.DecimalFormatUtil;
import com.itextpdf.io.util.StreamUtil;
import com.itextpdf.styledxmlparser.css.CssFontFaceRule;
import com.itextpdf.styledxmlparser.css.CssRuleSet;
import com.itextpdf.styledxmlparser.css.CssStatement;
import com.itextpdf.styledxmlparser.css.CssStyleSheet;
import com.itextpdf.styledxmlparser.css.ICssResolver;
import com.itextpdf.styledxmlparser.css.media.CssMediaRule;
import com.itextpdf.styledxmlparser.css.media.MediaDeviceDescription;
import com.itextpdf.styledxmlparser.css.page.PageMarginBoxContextNode;
import com.itextpdf.styledxmlparser.css.parse.CssRuleSetParser;
import com.itextpdf.styledxmlparser.css.parse.CssStyleSheetParser;
import com.itextpdf.styledxmlparser.css.pseudo.CssPseudoElementNode;
import com.itextpdf.styledxmlparser.css.resolve.AbstractCssContext;
import com.itextpdf.styledxmlparser.css.resolve.CssDefaults;
import com.itextpdf.styledxmlparser.css.resolve.CssInheritance;
import com.itextpdf.styledxmlparser.css.resolve.CssPropertyMerger;
import com.itextpdf.styledxmlparser.css.resolve.IStyleInheritance;
import com.itextpdf.styledxmlparser.css.util.CssUtils;
import com.itextpdf.styledxmlparser.node.IDataNode;
import com.itextpdf.styledxmlparser.node.IDocumentNode;
import com.itextpdf.styledxmlparser.node.IElementNode;
import com.itextpdf.styledxmlparser.node.INode;
import com.itextpdf.styledxmlparser.node.IStylesContainer;
import com.itextpdf.styledxmlparser.resolver.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Default implementation of the {@link ICssResolver} interface.
*/
public class DefaultCssResolver implements ICssResolver {
/**
* The CSS style sheet.
*/
private CssStyleSheet cssStyleSheet;
/**
* The device description.
*/
private MediaDeviceDescription deviceDescription;
/**
* Css inheritance checker
*/
private IStyleInheritance cssInheritance = new CssInheritance();
/**
* The list of fonts.
*/
private List fonts = new ArrayList<>();
private static final List fontSizeDependentPercentage = new ArrayList(2);
static {
fontSizeDependentPercentage.add(CssConstants.FONT_SIZE);
fontSizeDependentPercentage.add(CssConstants.LINE_HEIGHT);
}
/**
* Creates a new {@link DefaultCssResolver} instance.
*
* @param treeRoot the root node
* @param mediaDeviceDescription the media device description
* @param resourceResolver the resource resolver
*/
public DefaultCssResolver(INode treeRoot, MediaDeviceDescription mediaDeviceDescription, ResourceResolver resourceResolver) {
this.deviceDescription = mediaDeviceDescription;
collectCssDeclarations(treeRoot, resourceResolver, null);
collectFonts();
}
/**
* Creates a new {@link DefaultCssResolver} instance.
*
* @param treeRoot the root node
* @param context the processor context
*/
public DefaultCssResolver(INode treeRoot, ProcessorContext context) {
this.deviceDescription = context.getDeviceDescription();
collectCssDeclarations(treeRoot, context.getResourceResolver(), context.getCssContext());
collectFonts();
}
@Override
public Map resolveStyles(INode element, AbstractCssContext context) {
if (context instanceof CssContext) {
return resolveStyles(element, (CssContext) context);
}
throw new Html2PdfException("custom AbstractCssContext implementations are not supported yet");
}
/* (non-Javadoc)
* @see com.itextpdf.html2pdf.css.resolve.ICssResolver#resolveStyles(com.itextpdf.html2pdf.html.node.INode, com.itextpdf.html2pdf.css.resolve.CssContext)
*/
private Map resolveStyles(INode element, CssContext context) {
List ruleSets = new ArrayList<>();
ruleSets.add(new CssRuleSet(null, UserAgentCss.getStyles(element)));
if (element instanceof IElementNode) {
ruleSets.add(new CssRuleSet(null, HtmlStylesToCssConverter.convert((IElementNode) element)));
}
ruleSets.addAll(cssStyleSheet.getCssRuleSets(element, deviceDescription));
if (element instanceof IElementNode) {
String styleAttribute = ((IElementNode) element).getAttribute(AttributeConstants.STYLE);
if (styleAttribute != null) {
ruleSets.add(new CssRuleSet(null, CssRuleSetParser.parsePropertyDeclarations(styleAttribute)));
}
}
Map elementStyles = CssStyleSheet.extractStylesFromRuleSets(ruleSets);
if (CssConstants.CURRENTCOLOR.equals(elementStyles.get(CssConstants.COLOR))) {
// css-color-3/#currentcolor:
// If the ‘currentColor’ keyword is set on the ‘color’ property itself, it is treated as ‘color: inherit’.
elementStyles.put(CssConstants.COLOR, CssConstants.INHERIT);
}
String parentFontSizeStr = null;
if (element.parentNode() instanceof IStylesContainer) {
IStylesContainer parentNode = (IStylesContainer) element.parentNode();
Map parentStyles = parentNode.getStyles();
if (parentStyles == null && !(element.parentNode() instanceof IDocumentNode)) {
Logger logger = LoggerFactory.getLogger(DefaultCssResolver.class);
logger.error(LogMessageConstant.ERROR_RESOLVING_PARENT_STYLES);
}
if (parentStyles != null) {
for (Map.Entry entry : parentStyles.entrySet()) {
mergeParentCssDeclaration(elementStyles, entry.getKey(), entry.getValue(), parentStyles);
}
parentFontSizeStr = parentStyles.get(CssConstants.FONT_SIZE);
}
}
String elementFontSize = elementStyles.get(CssConstants.FONT_SIZE);
if (CssUtils.isRelativeValue(elementFontSize) || CssConstants.LARGER.equals(elementFontSize) || CssConstants.SMALLER.equals(elementFontSize)) {
float baseFontSize;
if (CssUtils.isRemValue(elementFontSize)) {
baseFontSize = context.getRootFontSize();
} else {
if (parentFontSizeStr == null) {
baseFontSize = CssUtils.parseAbsoluteFontSize(CssDefaults.getDefaultValue(CssConstants.FONT_SIZE));
} else {
baseFontSize = CssUtils.parseAbsoluteLength(parentFontSizeStr);
}
}
float absoluteFontSize = CssUtils.parseRelativeFontSize(elementFontSize, baseFontSize);
// Format to 4 decimal places to prevent differences between Java and C#
elementStyles.put(CssConstants.FONT_SIZE, DecimalFormatUtil.formatNumber(absoluteFontSize, "0.####") + CssConstants.PT);
} else {
elementStyles.put(CssConstants.FONT_SIZE, Float.toString(CssUtils.parseAbsoluteFontSize(elementFontSize)) + CssConstants.PT);
}
// Update root font size
if (element instanceof IElementNode && TagConstants.HTML.equals(((IElementNode) element).name())) {
context.setRootFontSize(elementStyles.get(CssConstants.FONT_SIZE));
}
Set keys = new HashSet<>();
for (Map.Entry entry : elementStyles.entrySet()) {
if (CssConstants.INITIAL.equals(entry.getValue())
|| CssConstants.INHERIT.equals(entry.getValue())) { // if "inherit" is not resolved till now, parents don't have it
keys.add(entry.getKey());
}
}
for (String key : keys) {
elementStyles.put(key, CssDefaults.getDefaultValue(key));
}
// This is needed for correct resolving of content property, so doing it right here
CounterProcessorUtil.processCounters(elementStyles, context, element);
resolveContentProperty(elementStyles, element, context);
return elementStyles;
}
/**
* Gets the list of fonts.
*
* @return the list of {@link CssFontFaceRule} instances
*/
public List getFonts() {
return fonts;
}
/**
* Resolves a content property.
*
* @param styles the styles map
* @param contentContainer the content container
* @param context the CSS context
*/
private void resolveContentProperty(Map styles, INode contentContainer, CssContext context) {
if (contentContainer instanceof CssPseudoElementNode || contentContainer instanceof PageMarginBoxContextNode) {
List resolvedContent = CssContentPropertyResolver.resolveContent(styles, contentContainer, context);
if (resolvedContent != null) {
for (INode child : resolvedContent) {
contentContainer.addChild(child);
}
}
}
}
/**
* Collects CSS declarationss.
*
* @param rootNode the root node
* @param resourceResolver the resource resolver
* @param cssContext the CSS context
*/
private void collectCssDeclarations(INode rootNode, ResourceResolver resourceResolver, CssContext cssContext) {
cssStyleSheet = new CssStyleSheet();
LinkedList q = new LinkedList<>();
q.add(rootNode);
while (!q.isEmpty()) {
INode currentNode = q.getFirst();
q.removeFirst();
if (currentNode instanceof IElementNode) {
IElementNode headChildElement = (IElementNode) currentNode;
if (TagConstants.STYLE.equals(headChildElement.name())) {
if (currentNode.childNodes().size() > 0 && currentNode.childNodes().get(0) instanceof IDataNode) {
String styleData = ((IDataNode) currentNode.childNodes().get(0)).getWholeData();
checkIfPagesCounterMentioned(styleData, cssContext);
CssStyleSheet styleSheet = CssStyleSheetParser.parse(styleData);
styleSheet = wrapStyleSheetInMediaQueryIfNecessary(headChildElement, styleSheet);
cssStyleSheet.appendCssStyleSheet(styleSheet);
}
} else if (HtmlUtils.isStyleSheetLink(headChildElement)) {
String styleSheetUri = headChildElement.getAttribute(AttributeConstants.HREF);
try {
InputStream stream = resourceResolver.retrieveStyleSheet(styleSheetUri);
byte[] bytes = StreamUtil.inputStreamToArray(stream);
checkIfPagesCounterMentioned(new String(bytes, StandardCharsets.UTF_8), cssContext);
CssStyleSheet styleSheet = CssStyleSheetParser.parse(new ByteArrayInputStream(bytes), resourceResolver.resolveAgainstBaseUri(styleSheetUri).toExternalForm());
styleSheet = wrapStyleSheetInMediaQueryIfNecessary(headChildElement, styleSheet);
cssStyleSheet.appendCssStyleSheet(styleSheet);
} catch (Exception exc) {
Logger logger = LoggerFactory.getLogger(DefaultCssResolver.class);
logger.error(LogMessageConstant.UNABLE_TO_PROCESS_EXTERNAL_CSS_FILE, exc);
}
}
}
for (INode child : currentNode.childNodes()) {
if (child instanceof IElementNode) {
q.add(child);
}
}
}
}
/**
* Check if a pages counter is mentioned.
*
* @param cssContents the CSS contents
* @param cssContext the CSS context
*/
private void checkIfPagesCounterMentioned(String cssContents, CssContext cssContext) {
// TODO more efficient (avoid searching in text string) and precise (e.g. skip spaces) check during the parsing.
if (cssContents.contains("counter(pages)") || cssContents.contains("counters(pages")) {
// The presence of counter(pages) means that theoretically relayout may be needed.
// We don't know it yet because that selector might not even be used, but
// when we know it for sure, it's too late because the Document is created right in the start.
cssContext.setPagesCounterPresent(true);
}
}
/**
* Wraps a {@link CssMediaRule} into the style sheet if the head child element has a media attribute.
*
* @param headChildElement the head child element
* @param styleSheet the style sheet
* @return the css style sheet
*/
private CssStyleSheet wrapStyleSheetInMediaQueryIfNecessary(IElementNode headChildElement, CssStyleSheet styleSheet) {
String mediaAttribute = headChildElement.getAttribute(AttributeConstants.MEDIA);
if (mediaAttribute != null && mediaAttribute.length() > 0) {
CssMediaRule mediaRule = new CssMediaRule(mediaAttribute);
mediaRule.addStatementsToBody(styleSheet.getStatements());
styleSheet = new CssStyleSheet();
styleSheet.addStatement(mediaRule);
}
return styleSheet;
}
/**
* Merge parent CSS declarations.
*
* @param styles the styles map
* @param cssProperty the CSS property
* @param parentPropValue the parent properties value
*/
private void mergeParentCssDeclaration(Map styles, String cssProperty, String parentPropValue, Map parentStyles) {
String childPropValue = styles.get(cssProperty);
if ((childPropValue == null && cssInheritance.isInheritable(cssProperty)) || CssConstants.INHERIT.equals(childPropValue)) {
if (valueIsOfMeasurement(parentPropValue, CssConstants.EM) || valueIsOfMeasurement(parentPropValue, CssConstants.EX) ||
valueIsOfMeasurement(parentPropValue, CssConstants.PERCENTAGE) && fontSizeDependentPercentage.contains(cssProperty)) {
float absoluteParentFontSize = CssUtils.parseAbsoluteLength(parentStyles.get(CssConstants.FONT_SIZE));
// Format to 4 decimal places to prevent differences between Java and C#
styles.put(cssProperty, DecimalFormatUtil.formatNumber(CssUtils.parseRelativeValue(parentPropValue, absoluteParentFontSize),
"0.####") + CssConstants.PT);
} else
styles.put(cssProperty, parentPropValue);
} else if (CssConstants.TEXT_DECORATION.equals(cssProperty) && !CssConstants.INLINE_BLOCK.equals(styles.get(CssConstants.DISPLAY))) {
// TODO Note! This property is formally not inherited, but the browsers behave very similar to inheritance here.
/* Text decorations on inline boxes are drawn across the entire element,
going across any descendant elements without paying any attention to their presence. */
// Also, when, for example, parent element has text-decoration:underline, and the child text-decoration:overline,
// then the text in the child will be both overline and underline. This is why the declarations are merged
// See TextDecorationTest#textDecoration01Test
styles.put(cssProperty, CssPropertyMerger.mergeTextDecoration(childPropValue, parentPropValue));
}
}
private static boolean valueIsOfMeasurement(String value, String measurement) {
if (value == null)
return false;
if (value.endsWith(measurement) && CssUtils.isNumericValue(value.substring(0, value.length() - measurement.length()).trim()))
return true;
return false;
}
/**
* Collects fonts from the style sheet.
*/
private void collectFonts() {
for (CssStatement cssStatement : cssStyleSheet.getStatements()) {
collectFonts(cssStatement);
}
}
/**
* Collects fonts from a {@link CssStatement}.
*
* @param cssStatement the CSS statement
*/
private void collectFonts(CssStatement cssStatement) {
if (cssStatement instanceof CssFontFaceRule) {
fonts.add((CssFontFaceRule) cssStatement);
} else if (cssStatement instanceof CssMediaRule && ((CssMediaRule) cssStatement).matchMediaDevice(deviceDescription)) {
for (CssStatement cssSubStatement : ((CssMediaRule) cssStatement).getStatements()) {
collectFonts(cssSubStatement);
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy