com.openhtmltopdf.css.style.CalculatedStyle Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openhtmltopdf-core Show documentation
Show all versions of openhtmltopdf-core Show documentation
Open HTML to PDF is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code.
The newest version!
/*
* CalculatedStyle.java
* Copyright (c) 2004, 2005 Patrick Wright, Torbjoern Gannholm
* Copyright (c) 2006 Wisconsin Court System
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
package com.openhtmltopdf.css.style;
import java.awt.Cursor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
import com.openhtmltopdf.context.StyleReference;
import com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.newmatch.CascadedStyle;
import com.openhtmltopdf.css.parser.CSSPrimitiveValue;
import com.openhtmltopdf.css.parser.CounterData;
import com.openhtmltopdf.css.parser.FSColor;
import com.openhtmltopdf.css.parser.FSFunction;
import com.openhtmltopdf.css.parser.FSRGBColor;
import com.openhtmltopdf.css.parser.PropertyValue;
import com.openhtmltopdf.css.parser.property.PrimitivePropertyBuilders;
import com.openhtmltopdf.css.sheet.PropertyDeclaration;
import com.openhtmltopdf.css.style.derived.BorderPropertySet;
import com.openhtmltopdf.css.style.derived.CountersValue;
import com.openhtmltopdf.css.style.derived.DerivedValueFactory;
import com.openhtmltopdf.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.css.style.derived.FunctionValue;
import com.openhtmltopdf.css.style.derived.LengthValue;
import com.openhtmltopdf.css.style.derived.ListValue;
import com.openhtmltopdf.css.style.derived.NumberValue;
import com.openhtmltopdf.css.style.derived.RectPropertySet;
import com.openhtmltopdf.css.value.FontSpecification;
import com.openhtmltopdf.layout.BoxBuilder;
import com.openhtmltopdf.layout.LayoutContext;
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.layout.counter.RootCounterContext;
import com.openhtmltopdf.newtable.TableBox;
import com.openhtmltopdf.render.Box;
import com.openhtmltopdf.render.FSFont;
import com.openhtmltopdf.render.FSFontMetrics;
import com.openhtmltopdf.render.RenderingContext;
import com.openhtmltopdf.util.LogMessageId;
import com.openhtmltopdf.util.ThreadCtx;
import com.openhtmltopdf.util.WebDoc;
import com.openhtmltopdf.util.WebDocLocations;
import com.openhtmltopdf.util.XRLog;
import com.openhtmltopdf.util.XRRuntimeException;
/**
* A set of properties that apply to a single Element, derived from all matched
* properties following the rules for CSS cascade, inheritance, importance,
* specificity and sequence. A property retrieved by name should always have
* exactly one value in this class (e.g. one-one map). Some methods to retrieve
* property values from an instance of this class require a valid {@link
* com.openhtmltopdf.layout.LayoutContext} be given to them.
*
* This is the go to class for working with a resolved style. Generally, you can get a
* instance for a box by calling:
*
*
* - {@link Box#getStyle()} after a box has been created by the {@link BoxBuilder}
* - {@link SharedContext#getStyle(org.w3c.dom.Element)} for an element
* - {@link StyleReference#getPseudoElementStyle(org.w3c.dom.Node, String)} for a pseudo
* element. StyleReference is available from {@link LayoutContext}
* - {@link #deriveStyle(CascadedStyle)}
to create a child style (non-inherited
* property values will not be available from the child style
* - {@link EmptyStyle} to start with nothing
*
*
* @author Torbjoern Gannholm
* @author Patrick Wright
*/
public class CalculatedStyle {
/**
* The parent-style we inherit from
*/
private CalculatedStyle _parent;
private BorderPropertySet _border;
private RectPropertySet _margin;
private RectPropertySet _padding;
private float _lineHeight;
private boolean _lineHeightResolved;
private FSFont _FSFont;
private FSFontMetrics _FSFontMetrics;
private boolean _marginsAllowed = true;
private boolean _paddingAllowed = true;
private boolean _bordersAllowed = true;
/**
* Cache child styles of this style that have the same cascaded properties
*/
private final java.util.Map _childCache = new java.util.HashMap<>();
/**
* Our main array of property values defined in this style, keyed
* by the CSSName assigned ID.
*/
private final FSDerivedValue[] _derivedValuesById;
public FSDerivedValue[] getderivedValuesById() {
return _derivedValuesById;
}
/**
* The derived Font for this style
*/
private FontSpecification _font;
/**
* Default constructor; as the instance is immutable after use, don't use
* this for class instantiation externally.
*/
protected CalculatedStyle() {
_derivedValuesById = new FSDerivedValue[CSSName.countCSSPrimitiveNames()];
}
/**
* Constructor for the CalculatedStyle object. To get a derived style, use
* the Styler objects getDerivedStyle which will cache styles
*
* @param parent PARAM
* @param matched PARAM
*/
private CalculatedStyle(CalculatedStyle parent, CascadedStyle matched) {
this();
_parent = parent;
init(matched);
}
private void init(CascadedStyle matched) {
derive(matched);
checkPaddingAllowed();
checkMarginsAllowed();
checkBordersAllowed();
}
private void checkPaddingAllowed() {
IdentValue v = getIdent(CSSName.DISPLAY);
if (v == IdentValue.TABLE_HEADER_GROUP || v == IdentValue.TABLE_ROW_GROUP ||
v == IdentValue.TABLE_FOOTER_GROUP || v == IdentValue.TABLE_ROW) {
_paddingAllowed = false;
} else if ((v == IdentValue.TABLE || v == IdentValue.INLINE_TABLE) && isCollapseBorders()) {
_paddingAllowed = false;
}
}
private void checkMarginsAllowed() {
IdentValue v = getIdent(CSSName.DISPLAY);
if (v == IdentValue.TABLE_HEADER_GROUP || v == IdentValue.TABLE_ROW_GROUP ||
v == IdentValue.TABLE_FOOTER_GROUP || v == IdentValue.TABLE_ROW ||
v == IdentValue.TABLE_CELL) {
_marginsAllowed = false;
}
}
private void checkBordersAllowed() {
IdentValue v = getIdent(CSSName.DISPLAY);
if (v == IdentValue.TABLE_HEADER_GROUP || v == IdentValue.TABLE_ROW_GROUP ||
v == IdentValue.TABLE_FOOTER_GROUP || v == IdentValue.TABLE_ROW) {
_bordersAllowed = false;
}
}
/**
* Derives a child style from this style. Non-inherited properties
* such as borders will be replaced compared to this
which is used
* as parent style.
*
* Depends on the ability to return the identical CascadedStyle each time a child style is needed
*
* @param matched the CascadedStyle to apply
* @return The derived child style
*/
public CalculatedStyle deriveStyle(CascadedStyle matched) {
String fingerprint = matched.getFingerprint();
CalculatedStyle cs = _childCache.get(fingerprint);
if (cs == null) {
cs = new CalculatedStyle(this, matched);
_childCache.put(fingerprint, cs);
}
RootCounterContext cc = ThreadCtx.get().sharedContext().getGlobalCounterContext();
cc.resetCounterValue(cs);
cc.incrementCounterValue(cs);
return cs;
}
/**
* Override this style with specified styles. This will NOT
* create a child style, rather an exact copy with only the specified
* properties overridden. Compare to {@link #deriveStyle(CascadedStyle)}.
*/
public CalculatedStyle overrideStyle(CascadedStyle matched) {
CalculatedStyle ret = new CalculatedStyle();
ret._parent = this._parent;
System.arraycopy(this._derivedValuesById, 0, ret._derivedValuesById, 0, ret._derivedValuesById.length);
init(matched);
return ret;
}
/**
* Override this style with specified styles. This will NOT
* create a child style, rather an exact copy with only the display
* property overridden. Compare to {@link #createAnonymousStyle(IdentValue)}
* which creates a child style.
*/
public CalculatedStyle overrideStyle(IdentValue display) {
CalculatedStyle ret = new CalculatedStyle();
ret._parent = this._parent;
System.arraycopy(this._derivedValuesById, 0, ret._derivedValuesById, 0, ret._derivedValuesById.length);
ret.init(CascadedStyle.createAnonymousStyle(display));
return ret;
}
/**
* Returns the parent style.
*
* @return Returns the parent style
*/
public CalculatedStyle getParent() {
return _parent;
}
/**
* Converts to a String representation of the object.
*
* @return The borderWidth value
*/
@Override
public String toString() {
return genStyleKey();
}
public FSColor asColor(CSSName cssName) {
FSDerivedValue prop = valueByName(cssName);
if (prop == IdentValue.TRANSPARENT) {
return FSRGBColor.TRANSPARENT;
} else {
return prop.asColor();
}
}
public float asFloat(CSSName cssName) {
return valueByName(cssName).asFloat();
}
public String asString(CSSName cssName) {
return valueByName(cssName).asString();
}
public String[] asStringArray(CSSName cssName) {
return valueByName(cssName).asStringArray();
}
// TODO: doc
public boolean hasAbsoluteUnit(CSSName cssName) {
boolean isAbs = false;
try {
isAbs = valueByName(cssName).hasAbsoluteUnit();
} catch (Exception e) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId2Param.LAYOUT_CSS_PROPERTY_HAS_UNPROCESSABLE_ASSIGNMENT, cssName, e.getMessage());
isAbs = false;
}
return isAbs;
}
/**
* Gets the ident attribute of the CalculatedStyle object
*
* @param cssName PARAM
* @param val PARAM
* @return The ident value
*/
public boolean isIdent(CSSName cssName, IdentValue val) {
return valueByName(cssName) == val;
}
/**
* Gets the ident attribute of the CalculatedStyle object
*
* @param cssName PARAM
* @return The ident value
*/
public IdentValue getIdent(CSSName cssName) {
return valueByName(cssName).asIdentValue();
}
/**
* Convenience property accessor; returns a Color initialized with the
* foreground color Uses the actual value (computed actual value) for this
* element.
*
* @return The color value
*/
public FSColor getColor() {
return asColor(CSSName.COLOR);
}
/**
* Convenience property accessor; returns a Color initialized with the
* background color value; Uses the actual value (computed actual value) for
* this element.
*
* @return The backgroundColor value
*/
public FSColor getBackgroundColor() {
FSDerivedValue prop = valueByName(CSSName.BACKGROUND_COLOR);
if (prop == IdentValue.TRANSPARENT) {
return null;
} else {
return asColor(CSSName.BACKGROUND_COLOR);
}
}
public List getCounterReset() {
FSDerivedValue value = valueByName(CSSName.COUNTER_RESET);
if (value == IdentValue.NONE) {
return null;
} else {
return ((CountersValue) value).getValues();
}
}
public List getCounterIncrement() {
FSDerivedValue value = valueByName(CSSName.COUNTER_INCREMENT);
if (value == IdentValue.NONE) {
return null;
} else {
return ((CountersValue) value).getValues();
}
}
public BorderPropertySet getBorder(CssContext ctx) {
if (! _bordersAllowed) {
return BorderPropertySet.EMPTY_BORDER;
} else {
BorderPropertySet b = getBorderProperty(this, ctx);
return b;
}
}
public FontSpecification getFont(CssContext ctx) {
if (_font == null) {
_font = new FontSpecification();
_font.families = valueByName(CSSName.FONT_FAMILY).asStringArray();
FSDerivedValue fontSize = valueByName(CSSName.FONT_SIZE);
if (fontSize instanceof IdentValue) {
PropertyValue replacement;
IdentValue resolved = resolveAbsoluteFontSize();
if (resolved != null) {
replacement = FontSizeHelper.resolveAbsoluteFontSize(resolved, _font.families);
} else {
replacement = FontSizeHelper.getDefaultRelativeFontSize((IdentValue) fontSize);
}
_font.size = LengthValue.calcFloatProportionalValue(
this, CSSName.FONT_SIZE, replacement.getCssText(),
replacement.getFloatValue(), replacement.getPrimitiveType(), 0, ctx);
} else {
_font.size = getFloatPropertyProportionalTo(CSSName.FONT_SIZE, 0, ctx);
}
_font.fontWeight = getIdent(CSSName.FONT_WEIGHT);
_font.fontStyle = getIdent(CSSName.FONT_STYLE);
_font.variant = getIdent(CSSName.FONT_VARIANT);
}
return _font;
}
public FontSpecification getFontSpecification() {
return _font;
}
private IdentValue resolveAbsoluteFontSize() {
FSDerivedValue fontSize = valueByName(CSSName.FONT_SIZE);
if (! (fontSize instanceof IdentValue)) {
return null;
}
IdentValue fontSizeIdent = (IdentValue) fontSize;
if (PrimitivePropertyBuilders.ABSOLUTE_FONT_SIZES.get(fontSizeIdent.FS_ID)) {
return fontSizeIdent;
}
IdentValue parent = getParent().resolveAbsoluteFontSize();
if (parent != null) {
if (fontSizeIdent == IdentValue.SMALLER) {
return FontSizeHelper.getNextSmaller(parent);
} else if (fontSize == IdentValue.LARGER) {
return FontSizeHelper.getNextLarger(parent);
}
}
return null;
}
public float getFloatPropertyProportionalTo(CSSName cssName, float baseValue, CssContext ctx) {
return valueByName(cssName).getFloatProportionalTo(cssName, baseValue, ctx);
}
/**
* @param cssName
* @param parentWidth
* @param ctx
* @return TODO
*/
public float getFloatPropertyProportionalWidth(CSSName cssName, float parentWidth, CssContext ctx) {
return valueByName(cssName).getFloatProportionalTo(cssName, parentWidth, ctx);
}
/**
* @param cssName
* @param parentHeight
* @param ctx
* @return TODO
*/
public float getFloatPropertyProportionalHeight(CSSName cssName, float parentHeight, CssContext ctx) {
return valueByName(cssName).getFloatProportionalTo(cssName, parentHeight, ctx);
}
public float getLineHeight(CssContext ctx) {
if (! _lineHeightResolved) {
if (isIdent(CSSName.LINE_HEIGHT, IdentValue.NORMAL)) {
float lineHeight1 = getFont(ctx).size * 1.1f;
// Make sure rasterized characters will (probably) fit inside
// the line box
FSFontMetrics metrics = getFSFontMetrics(ctx);
float lineHeight2 = (float)Math.ceil(metrics.getDescent() + metrics.getAscent());
_lineHeight = Math.max(lineHeight1, lineHeight2);
} else if (isLength(CSSName.LINE_HEIGHT)) {
//could be more elegant, I suppose
_lineHeight = getFloatPropertyProportionalHeight(CSSName.LINE_HEIGHT, 0, ctx);
} else {
//must be a number
_lineHeight = getFont(ctx).size * valueByName(CSSName.LINE_HEIGHT).asFloat();
}
_lineHeightResolved = true;
}
return _lineHeight;
}
/**
* Convenience property accessor; returns a Border initialized with the
* four-sided margin width. Uses the actual value (computed actual value)
* for this element.
*
* @param cbWidth
* @param ctx
* @return The marginWidth value
*/
public RectPropertySet getMarginRect(float cbWidth, CssContext ctx) {
return getMarginRect(cbWidth, ctx, true);
}
public RectPropertySet getMarginRect(float cbWidth, CssContext ctx, boolean useCache) {
if (! _marginsAllowed) {
return RectPropertySet.ALL_ZEROS;
} else {
return getMarginProperty(
this, CSSName.MARGIN_SHORTHAND, CSSName.MARGIN_SIDE_PROPERTIES, cbWidth, ctx, useCache);
}
}
/**
* Convenience property accessor; returns a Border initialized with the
* four-sided padding width. Uses the actual value (computed actual value)
* for this element.
*
* @param cbWidth
* @param ctx
* @return The paddingWidth value
*/
public RectPropertySet getPaddingRect(float cbWidth, CssContext ctx, boolean useCache) {
if (! _paddingAllowed) {
return RectPropertySet.ALL_ZEROS;
} else {
return getPaddingProperty(this, CSSName.PADDING_SHORTHAND, CSSName.PADDING_SIDE_PROPERTIES, cbWidth, ctx, useCache);
}
}
public RectPropertySet getPaddingRect(float cbWidth, CssContext ctx) {
return getPaddingRect(cbWidth, ctx, true);
}
/**
* @param cssName
* @return TODO
*/
public String getStringProperty(CSSName cssName) {
return valueByName(cssName).asString();
}
/**
* TODO: doc
*/
public boolean isLength(CSSName cssName) {
FSDerivedValue val = valueByName(cssName);
return val instanceof LengthValue;
}
public boolean isLengthOrNumber(CSSName cssName) {
FSDerivedValue val = valueByName(cssName);
return val instanceof NumberValue || val instanceof LengthValue;
}
/**
* Returns a {@link FSDerivedValue} by name. Because we are a derived
* style, the property will already be resolved at this point.
*
* This will look up the ancestor tree for inherited properties and
* use an initial value for unspecified properties which do not inherit.
*
* @param cssName The CSS property name, e.g. "font-family"
*/
public FSDerivedValue valueByName(CSSName cssName) {
FSDerivedValue val = _derivedValuesById[cssName.FS_ID];
boolean needInitialValue = val == IdentValue.FS_INITIAL_VALUE;
// but the property may not be defined for this Element
if (val == null || needInitialValue) {
// if it is inheritable (like color) and we are not root, ask our parent
// for the value
if (! needInitialValue && CSSName.propertyInherits(cssName)
&& _parent != null
&& (val = _parent.valueByName(cssName)) != null) {
// Do nothing, val is already set
} else {
// otherwise, use the initial value (defined by the CSS2 Spec)
String initialValue = CSSName.initialValue(cssName);
if (initialValue == null) {
throw new XRRuntimeException("Property '" + cssName + "' has no initial values assigned. " +
"Check CSSName declarations.");
}
if (initialValue.charAt(0) == '=') {
CSSName ref = CSSName.getByPropertyName(initialValue.substring(1));
val = valueByName(ref);
} else {
val = CSSName.initialDerivedValue(cssName);
}
}
_derivedValuesById[cssName.FS_ID] = val;
}
return val;
}
/**
* This method should result in the element for this style having a
* derived value for all specified (in stylesheets, style attribute, other non
* CSS attrs, etc) primitive CSS properties. Other properties are picked up
* from an ancestor (if they inherit) or their initial values (if
* they don't inherit). See {@link #valueByName(CSSName)}.
*
* The implementation is based on the notion that
* the matched styles are given to us in a perfectly sorted order, such that
* properties appearing later in the rule-set always override properties
* appearing earlier.
*
* The current implementation makes no attempt to check
* this assumption. When this method exits, the derived property
* list for this class will be populated with the properties defined for
* this element, properly cascaded.
*/
private void derive(CascadedStyle matched) {
if (matched == null) {
return;
}
for (PropertyDeclaration pd : matched.getCascadedPropertyDeclarations()) {
FSDerivedValue val = deriveValue(pd.getCSSName(), pd.getValue());
_derivedValuesById[pd.getCSSName().FS_ID] = val;
}
}
private FSDerivedValue deriveValue(CSSName cssName, CSSPrimitiveValue value) {
return DerivedValueFactory.newDerivedValue(this, cssName, (PropertyValue) value);
}
private String genStyleKey() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < _derivedValuesById.length; i++) {
CSSName name = CSSName.getByID(i);
FSDerivedValue val = _derivedValuesById[i];
if (val != null) {
sb.append(name.toString());
} else {
sb.append("(no prop assigned in this pos)");
}
sb.append("|\n");
}
return sb.toString();
}
public RectPropertySet getCachedPadding() {
if (_padding == null) {
throw new XRRuntimeException("No padding property cached yet; should have called getPropertyRect() at least once before.");
} else {
return _padding;
}
}
public RectPropertySet getCachedMargin() {
if (_margin == null) {
throw new XRRuntimeException("No margin property cached yet; should have called getMarginRect() at least once before.");
} else {
return _margin;
}
}
private static RectPropertySet getPaddingProperty(CalculatedStyle style,
CSSName shorthandProp,
CSSName.CSSSideProperties sides,
float cbWidth,
CssContext ctx,
boolean useCache) {
if (! useCache) {
return newRectInstance(style, shorthandProp, sides, cbWidth, ctx);
} else {
if (style._padding == null) {
RectPropertySet result = newRectInstance(style, shorthandProp, sides, cbWidth, ctx);
boolean allZeros = result.isAllZeros();
if (allZeros) {
result = RectPropertySet.ALL_ZEROS;
}
style._padding = result;
if (! allZeros && style._padding.hasNegativeValues()) {
style._padding.resetNegativeValues();
}
}
return style._padding;
}
}
private static RectPropertySet getMarginProperty(CalculatedStyle style,
CSSName shorthandProp,
CSSName.CSSSideProperties sides,
float cbWidth,
CssContext ctx,
boolean useCache) {
if (! useCache) {
return newRectInstance(style, shorthandProp, sides, cbWidth, ctx);
} else {
if (style._margin == null) {
RectPropertySet result = newRectInstance(style, shorthandProp, sides, cbWidth, ctx);
if (result.isAllZeros()) {
result = RectPropertySet.ALL_ZEROS;
}
style._margin = result;
}
return style._margin;
}
}
private static RectPropertySet newRectInstance(CalculatedStyle style,
CSSName shorthand,
CSSName.CSSSideProperties sides,
float cbWidth,
CssContext ctx) {
RectPropertySet rect;
rect = RectPropertySet.newInstance(style,
shorthand,
sides,
cbWidth,
ctx);
return rect;
}
private static BorderPropertySet getBorderProperty(CalculatedStyle style,
CssContext ctx) {
if (style._border == null) {
BorderPropertySet result = BorderPropertySet.newInstance(style, ctx);
boolean allZeros = result.isAllZeros();
if (allZeros && ! result.hasHidden() && !result.hasBorderRadius()) {
result = BorderPropertySet.EMPTY_BORDER;
}
style._border = result;
if (! allZeros && style._border.hasNegativeValues()) {
style._border.resetNegativeValues();
}
}
return style._border;
}
public static final int LEFT = 1;
public static final int RIGHT = 2;
public static final int TOP = 3;
public static final int BOTTOM = 4;
public int getMarginBorderPadding(
CssContext cssCtx, int cbWidth, int which) {
BorderPropertySet border = getBorder(cssCtx);
RectPropertySet margin = getMarginRect(cbWidth, cssCtx);
RectPropertySet padding = getPaddingRect(cbWidth, cssCtx);
switch (which) {
case LEFT:
return (int) (margin.left() + border.left() + padding.left());
case RIGHT:
return (int) (margin.right() + border.right() + padding.right());
case TOP:
return (int) (margin.top() + border.top() + padding.top());
case BOTTOM:
return (int) (margin.bottom() + border.bottom() + padding.bottom());
default:
throw new IllegalArgumentException();
}
}
public IdentValue getWhitespace() {
return getIdent(CSSName.WHITE_SPACE);
}
public FSFont getFSFont(CssContext cssContext) {
if (_FSFont == null) {
_FSFont = cssContext.getFont(getFont(cssContext));
}
return _FSFont;
}
public FSFontMetrics getFSFontMetrics(CssContext c) {
if (_FSFontMetrics == null) {
_FSFontMetrics = c.getFSFontMetrics(getFSFont(c));
}
return _FSFontMetrics;
}
public IdentValue getWordWrap() {
return getIdent(CSSName.WORD_WRAP);
}
public boolean isClearLeft() {
IdentValue clear = getIdent(CSSName.CLEAR);
return clear == IdentValue.LEFT || clear == IdentValue.BOTH;
}
public boolean isClearRight() {
IdentValue clear = getIdent(CSSName.CLEAR);
return clear == IdentValue.RIGHT || clear == IdentValue.BOTH;
}
public boolean isCleared() {
return ! isIdent(CSSName.CLEAR, IdentValue.NONE);
}
public IdentValue getBackgroundRepeat(PropertyValue value) {
return value.getIdentValue();
}
public boolean isInline() {
return isIdent(CSSName.DISPLAY, IdentValue.INLINE) &&
! (isFloated() || isAbsolute() || isFixed() || isRunning());
}
public boolean isInlineBlock() {
return isIdent(CSSName.DISPLAY, IdentValue.INLINE_BLOCK);
}
public boolean isTable() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE);
}
public boolean isInlineTable() {
return isIdent(CSSName.DISPLAY, IdentValue.INLINE_TABLE);
}
public boolean isTableCell() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE_CELL);
}
public boolean isTableSection() {
IdentValue display = getIdent(CSSName.DISPLAY);
return display == IdentValue.TABLE_ROW_GROUP ||
display == IdentValue.TABLE_HEADER_GROUP ||
display == IdentValue.TABLE_FOOTER_GROUP;
}
public boolean isTableCaption() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE_CAPTION);
}
public boolean isTableHeader() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE_HEADER_GROUP);
}
public boolean isTableFooter() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE_FOOTER_GROUP);
}
public boolean isTableRow() {
return isIdent(CSSName.DISPLAY, IdentValue.TABLE_ROW);
}
public boolean isDisplayNone() {
return isIdent(CSSName.DISPLAY, IdentValue.NONE);
}
public boolean isSpecifiedAsBlock() {
return isIdent(CSSName.DISPLAY, IdentValue.BLOCK);
}
public boolean isBlockEquivalent() {
if (isFloated() || isAbsolute() || isFixed()) {
return true;
} else {
IdentValue display = getIdent(CSSName.DISPLAY);
if (display == IdentValue.INLINE) {
return false;
} else {
return display == IdentValue.BLOCK || display == IdentValue.LIST_ITEM ||
display == IdentValue.RUN_IN || display == IdentValue.INLINE_BLOCK ||
display == IdentValue.TABLE || display == IdentValue.INLINE_TABLE;
}
}
}
public boolean isLayedOutInInlineContext() {
if (isFloated() || isAbsolute() || isFixed() || isRunning()) {
return true;
} else {
IdentValue display = getIdent(CSSName.DISPLAY);
return display == IdentValue.INLINE || display == IdentValue.INLINE_BLOCK ||
display == IdentValue.INLINE_TABLE;
}
}
public boolean isNeedAutoMarginResolution() {
return ! (isAbsolute() || isFixed() || isFloated() || isInlineBlock());
}
public boolean isAbsolute() {
return isIdent(CSSName.POSITION, IdentValue.ABSOLUTE);
}
public boolean isFixed() {
return isIdent(CSSName.POSITION, IdentValue.FIXED);
}
public boolean isFloated() {
IdentValue floatVal = getIdent(CSSName.FLOAT);
return floatVal == IdentValue.LEFT || floatVal == IdentValue.RIGHT;
}
public boolean isFloatedLeft() {
return isIdent(CSSName.FLOAT, IdentValue.LEFT);
}
public boolean isFloatedRight() {
return isIdent(CSSName.FLOAT, IdentValue.RIGHT);
}
public boolean isFootnote() {
return isIdent(CSSName.FLOAT, IdentValue.FOOTNOTE);
}
public boolean isFootnoteBody() {
return isIdent(CSSName.DISPLAY, IdentValue.FS_FOOTNOTE_BODY);
}
public boolean isRelative() {
return isIdent(CSSName.POSITION, IdentValue.RELATIVE);
}
public boolean isPostionedOrFloated() {
return isAbsolute() || isFixed() || isFloated() || isRelative();
}
public boolean isPositioned() {
return isAbsolute() || isFixed() || isRelative();
}
public boolean isAutoWidth() {
return isIdent(CSSName.WIDTH, IdentValue.AUTO);
}
public boolean isAbsoluteWidth() {
return valueByName(CSSName.WIDTH).hasAbsoluteUnit();
}
public boolean isAutoHeight() {
return isIdent(CSSName.HEIGHT, IdentValue.AUTO);
}
public boolean isAutoLeftMargin() {
return isIdent(CSSName.MARGIN_LEFT, IdentValue.AUTO);
}
public boolean isAutoRightMargin() {
return isIdent(CSSName.MARGIN_RIGHT, IdentValue.AUTO);
}
public boolean isAutoZIndex() {
return isIdent(CSSName.Z_INDEX, IdentValue.AUTO);
}
public boolean establishesBFC() {
if (hasColumns()) {
return true;
}
FSDerivedValue value = valueByName(CSSName.POSITION);
if (value instanceof FunctionValue) { // running(header)
return false;
} else {
IdentValue display = getIdent(CSSName.DISPLAY);
IdentValue position = (IdentValue)value;
return isFloated() ||
position == IdentValue.ABSOLUTE || position == IdentValue.FIXED ||
display == IdentValue.INLINE_BLOCK || display == IdentValue.TABLE_CELL ||
! isIdent(CSSName.OVERFLOW, IdentValue.VISIBLE);
}
}
public boolean requiresLayer() {
if (isIdent(CSSName.DISPLAY, IdentValue.INLINE)) {
// Layers must be block or block derived (including inline-block)
// according to modern html standards.
return false;
}
if (!isIdent(CSSName.TRANSFORM, IdentValue.NONE)) {
return true;
}
FSDerivedValue value = valueByName(CSSName.POSITION);
if (value instanceof FunctionValue) { // running(header)
return false;
} else {
IdentValue position = getIdent(CSSName.POSITION);
if (position == IdentValue.ABSOLUTE ||
position == IdentValue.RELATIVE || position == IdentValue.FIXED) {
return true;
}
IdentValue overflow = getIdent(CSSName.OVERFLOW);
return (overflow == IdentValue.SCROLL || overflow == IdentValue.AUTO) &&
isOverflowApplies();
}
}
public boolean isRunning() {
FSDerivedValue value = valueByName(CSSName.POSITION);
return value instanceof FunctionValue;
}
public String getRunningName() {
FunctionValue value = (FunctionValue)valueByName(CSSName.POSITION);
FSFunction function = value.getFunction();
PropertyValue param = function.getParameters().get(0);
return param.getStringValue();
}
public boolean isOverflowApplies() {
IdentValue display = getIdent(CSSName.DISPLAY);
return display == IdentValue.BLOCK || display == IdentValue.LIST_ITEM ||
display == IdentValue.TABLE || display == IdentValue.INLINE_BLOCK ||
display == IdentValue.TABLE_CELL;
}
public boolean isOverflowVisible() {
return valueByName(CSSName.OVERFLOW) == IdentValue.VISIBLE;
}
public boolean isHorizontalBackgroundRepeat(PropertyValue value) {
return value.getIdentValue() == IdentValue.REPEAT_X || value.getIdentValue() == IdentValue.REPEAT;
}
public boolean isVerticalBackgroundRepeat(PropertyValue value) {
return value.getIdentValue() == IdentValue.REPEAT_Y || value.getIdentValue() == IdentValue.REPEAT;
}
public boolean isTopAuto() {
return isIdent(CSSName.TOP, IdentValue.AUTO);
}
public boolean isBottomAuto() {
return isIdent(CSSName.BOTTOM, IdentValue.AUTO);
}
public boolean isListItem() {
return isIdent(CSSName.DISPLAY, IdentValue.LIST_ITEM);
}
public boolean hasColumns() {
return !isIdent(CSSName.COLUMN_COUNT, IdentValue.AUTO) && asFloat(CSSName.COLUMN_COUNT) > 1;
}
public int columnCount() {
return (int) asFloat(CSSName.COLUMN_COUNT);
}
public int fsMaxOverflowPages() {
return (int) asFloat(CSSName.FS_MAX_OVERFLOW_PAGES);
}
/**
* Determine if the element is visible. This is normaly the case
* if visibility == visible. Only when visibilty is
* -fs-table-paginate-repeated-visible and we are in a repeated table header
* the element will also be visible. This allows to only show an element in the table header
* after a page break.
* @param renderingContext null or the current renderingContext. If null,
* then the -fs-table-paginate-repeated-visible logic
* will not work.
* @param thisElement the element for which the visibility should be determined. Only required if
* -fs-table-paginate-repeated-visible is specified.
* @return true if the element is visible
*/
public boolean isVisible(RenderingContext renderingContext, Box thisElement) {
IdentValue val = getIdent(CSSName.VISIBILITY);
if (val == IdentValue.VISIBLE)
return true;
if (renderingContext != null) {
if (val == IdentValue.FS_TABLE_PAGINATE_REPEATED_VISIBLE) {
/*
* We need to find the parent TableBox which has a
* ContentLimitContainer and can be repeated.
*/
Box parentElement = thisElement.getParent();
while (parentElement != null
&& !(parentElement.getStyle().isTable()
&& ((TableBox) parentElement).hasContentLimitContainer()))
parentElement = parentElement.getDocumentParent();
if (parentElement != null) {
TableBox tableBox = (TableBox) parentElement;
return !tableBox.isTableRenderedOnFirstPage(renderingContext);
}
}
}
return false;
}
public boolean isForcePageBreakBefore() {
IdentValue val = getIdent(CSSName.PAGE_BREAK_BEFORE);
return val == IdentValue.ALWAYS || val == IdentValue.LEFT
|| val == IdentValue.RIGHT;
}
public boolean isForcePageBreakAfter() {
IdentValue val = getIdent(CSSName.PAGE_BREAK_AFTER);
return val == IdentValue.ALWAYS || val == IdentValue.LEFT
|| val == IdentValue.RIGHT;
}
public boolean isColumnBreakBefore() {
return isIdent(CSSName.BREAK_BEFORE, IdentValue.COLUMN);
}
public boolean isColumnBreakAfter() {
return isIdent(CSSName.BREAK_AFTER, IdentValue.COLUMN);
}
public boolean isAvoidPageBreakInside() {
return isIdent(CSSName.PAGE_BREAK_INSIDE, IdentValue.AVOID);
}
/**
* This method derives a style for an anonymous child box with an overriden
* value for the display property.
*
* NOTE: All non-inherited properties of this
will be lost as
* the returned style is for a child box.
*/
public CalculatedStyle createAnonymousStyle(IdentValue display) {
return deriveStyle(CascadedStyle.createAnonymousStyle(display));
}
public boolean mayHaveFirstLine() {
IdentValue display = getIdent(CSSName.DISPLAY);
return display == IdentValue.BLOCK ||
display == IdentValue.LIST_ITEM ||
display == IdentValue.RUN_IN ||
display == IdentValue.TABLE ||
display == IdentValue.TABLE_CELL ||
display == IdentValue.TABLE_CAPTION ||
display == IdentValue.INLINE_BLOCK;
}
public boolean mayHaveFirstLetter() {
IdentValue display = getIdent(CSSName.DISPLAY);
return display == IdentValue.BLOCK ||
display == IdentValue.LIST_ITEM ||
display == IdentValue.TABLE_CELL ||
display == IdentValue.TABLE_CAPTION ||
display == IdentValue.INLINE_BLOCK;
}
public boolean isNonFlowContent() {
return isFloated() || isAbsolute() || isFixed() || isRunning();
}
public boolean isMayCollapseMarginsWithChildren() {
return isIdent(CSSName.OVERFLOW, IdentValue.VISIBLE) &&
! (isFloated() || isAbsolute() || isFixed() || isInlineBlock());
}
public boolean isAbsFixedOrInlineBlockEquiv() {
return isAbsolute() || isFixed() || isInlineBlock() || isInlineTable();
}
public boolean isMaxWidthNone() {
return isIdent(CSSName.MAX_WIDTH, IdentValue.NONE);
}
public boolean isMaxHeightNone() {
return isIdent(CSSName.MAX_HEIGHT, IdentValue.NONE);
}
public boolean isImageRenderingPixelated() {
return isIdent(CSSName.IMAGE_RENDERING, IdentValue.PIXELATED) || isIdent(CSSName.IMAGE_RENDERING, IdentValue.CRISP_EDGES);
}
public boolean isImageRenderingInterpolate(){
return !isImageRenderingPixelated();
}
public int getMinWidth(CssContext c, int cbWidth) {
return (int) getFloatPropertyProportionalTo(CSSName.MIN_WIDTH, cbWidth, c);
}
public int getMaxWidth(CssContext c, int cbWidth) {
return (int) getFloatPropertyProportionalTo(CSSName.MAX_WIDTH, cbWidth, c);
}
public int getMinHeight(CssContext c, int cbHeight) {
return (int) getFloatPropertyProportionalTo(CSSName.MIN_HEIGHT, cbHeight, c);
}
public int getMaxHeight(CssContext c, int cbHeight) {
return (int) getFloatPropertyProportionalTo(CSSName.MAX_HEIGHT, cbHeight, c);
}
public boolean isCollapseBorders() {
// The second part of this condition was commented out by @danfickle because seems unneccessary
// See https://github.com/danfickle/openhtmltopdf/issues/97
return isIdent(CSSName.BORDER_COLLAPSE, IdentValue.COLLAPSE); // && ! isPaginateTable();
}
public int getBorderHSpacing(CssContext c) {
return isCollapseBorders() ? 0 : (int) getFloatPropertyProportionalTo(CSSName.FS_BORDER_SPACING_HORIZONTAL, 0, c);
}
public int getBorderVSpacing(CssContext c) {
return isCollapseBorders() ? 0 : (int) getFloatPropertyProportionalTo(CSSName.FS_BORDER_SPACING_VERTICAL, 0, c);
}
public int getRowSpan() {
int result = (int) asFloat(CSSName.FS_ROWSPAN);
return result > 0 ? result : 1;
}
public int getColSpan() {
int result = (int) asFloat(CSSName.FS_COLSPAN);
return result > 0 ? result : 1;
}
public float getFSPageBreakMinHeight(CssContext c){
return getFloatPropertyProportionalTo(CSSName.FS_PAGE_BREAK_MIN_HEIGHT, 0, c);
}
public Length asLength(CssContext c, CSSName cssName) {
Length result = new Length();
FSDerivedValue value = valueByName(cssName);
if (value instanceof LengthValue || value instanceof NumberValue) {
if (value.hasAbsoluteUnit()) {
result.setValue((int) value.getFloatProportionalTo(cssName, 0, c));
result.setType(Length.FIXED);
} else {
result.setValue((int) value.asFloat());
result.setType(Length.PERCENT);
}
}
return result;
}
public boolean isShowEmptyCells() {
return isCollapseBorders() || isIdent(CSSName.EMPTY_CELLS, IdentValue.SHOW);
}
public List getTextDecorations() {
FSDerivedValue value = valueByName(CSSName.TEXT_DECORATION);
if (value == IdentValue.NONE) {
return null;
} else {
List idents = ((ListValue) value).getValues();
return idents.stream()
.map(val -> (IdentValue) DerivedValueFactory.newDerivedValue(this, CSSName.TEXT_DECORATION, val))
.collect(Collectors.toList());
}
}
public Cursor getCursor() {
FSDerivedValue value = valueByName(CSSName.CURSOR);
if (value == IdentValue.AUTO || value == IdentValue.DEFAULT) {
return Cursor.getDefaultCursor();
} else if (value == IdentValue.CROSSHAIR) {
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
} else if (value == IdentValue.POINTER) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if (value == IdentValue.MOVE) {
return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
} else if (value == IdentValue.E_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
} else if (value == IdentValue.NE_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
} else if (value == IdentValue.NW_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
} else if (value == IdentValue.N_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
} else if (value == IdentValue.SE_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
} else if (value == IdentValue.SW_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
} else if (value == IdentValue.S_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
} else if (value == IdentValue.W_RESIZE) {
return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
} else if (value == IdentValue.TEXT) {
return Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
} else if (value == IdentValue.WAIT) {
return Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
} else if (value == IdentValue.HELP) {
// We don't have a cursor for this by default, maybe we need
// a custom one for this (but I don't like it).
return Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
} else if (value == IdentValue.PROGRESS) {
// We don't have a cursor for this by default, maybe we need
// a custom one for this (but I don't like it).
return Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
}
return null;
}
public boolean isPaginateTable() {
return isIdent(CSSName.FS_TABLE_PAGINATE, IdentValue.PAGINATE);
}
public boolean isTextJustify() {
return isIdent(CSSName.TEXT_ALIGN, IdentValue.JUSTIFY) &&
! (isIdent(CSSName.WHITE_SPACE, IdentValue.PRE) ||
isIdent(CSSName.WHITE_SPACE, IdentValue.PRE_LINE));
}
public boolean isListMarkerInside() {
return isIdent(CSSName.LIST_STYLE_POSITION, IdentValue.INSIDE);
}
public boolean isKeepWithInline() {
return isIdent(CSSName.FS_KEEP_WITH_INLINE, IdentValue.KEEP);
}
public boolean isDynamicAutoWidth() {
return isIdent(CSSName.FS_DYNAMIC_AUTO_WIDTH, IdentValue.DYNAMIC);
}
public boolean isDynamicAutoWidthApplicable() {
return isDynamicAutoWidth() && isAutoWidth() && ! isCanBeShrunkToFit();
}
public boolean isCanBeShrunkToFit() {
return isInlineBlock() || isFloated() || isAbsolute() || isFixed();
}
public boolean isDirectionLTR() {
return isIdent(CSSName.DIRECTION, IdentValue.LTR);
}
public boolean isDirectionRTL() {
return isIdent(CSSName.DIRECTION, IdentValue.RTL);
}
public boolean isDirectionAuto() {
return isIdent(CSSName.DIRECTION, IdentValue.AUTO);
}
public IdentValue getDirection() {
return getIdent(CSSName.DIRECTION);
}
public boolean hasLetterSpacing() {
return !isIdent(CSSName.LETTER_SPACING, IdentValue.NORMAL);
}
public boolean isParagraphContainerForBidi() {
IdentValue display = getIdent(CSSName.DISPLAY);
return (display != IdentValue.INLINE &&
display != IdentValue.INLINE_BLOCK &&
display != IdentValue.INLINE_TABLE) ||
isNonFlowContent();
}
/**
* @return true for border-box, false for content-box.
*/
public boolean isBorderBox() {
return isIdent(CSSName.BOX_SIZING, IdentValue.BORDER_BOX);
}
/**
* Aims to get the correct resolved max-width for a box in dots unit.
* Returns -1 if there is no max-width defined.
* Assumptions: box has a containing block.
*/
public static int getCSSMaxWidth(CssContext c, Box box) {
if (box.getStyle().isMaxWidthNone()) {
return -1;
}
return box.getStyle().getMaxWidth(c, box.getContainingBlock().getContentWidth());
}
/**
* Aims to get the correct resolved max-height for a box in dots unit.
* returns -1 if there is no max-height defined.
* Assumptions: box has a containing block.
*/
public static int getCSSMaxHeight(CssContext c, Box box) {
if (box.getStyle().isMaxHeightNone()) {
return -1;
}
Length cssMaxHeight = box.getStyle().asLength(c, CSSName.MAX_HEIGHT);
/* MDN says:
* The percentage is calculated with respect to the height of the generated box's containing block.
* If the height of the containing block is not specified explicitly (i.e., it depends on content height),
* and this element is not absolutely positioned, the percentage value is treated as none.*/
if (cssMaxHeight.isPercent() &&
box.getContainingBlock().getStyle().hasAbsoluteUnit(CSSName.HEIGHT)) {
return (int) (cssMaxHeight.value() * box.getContainingBlock().getStyle().asLength(c, CSSName.HEIGHT).value() / 100f);
} else if (cssMaxHeight.isPercent()) {
return -1;
} else {
return (int) cssMaxHeight.value();
}
}
public boolean isHasBackground() {
return !isIdent(CSSName.BACKGROUND_COLOR, IdentValue.TRANSPARENT) ||
isHasBackgroundImage();
}
public boolean isHasBackgroundImage() {
List backgroundImages = ((ListValue) valueByName(CSSName.BACKGROUND_IMAGE)).getValues();
if (backgroundImages.size() == 1) {
return backgroundImages.get(0).getIdentValue() != IdentValue.NONE;
} else {
return backgroundImages.stream().anyMatch(val -> val.getIdentValue() != IdentValue.NONE);
}
}
public enum BackgroundImageType {
URI, GRADIENT, NONE;
}
public static class BackgroundContainer {
public BackgroundImageType type;
public PropertyValue imageGradientOrNone;
public BackgroundPosition backgroundPosition;
public BackgroundSize backgroundSize;
public PropertyValue backgroundRepeat;
}
public boolean isLinearGradient(PropertyValue value) {
return value.getPropertyValueType() == PropertyValue.VALUE_TYPE_FUNCTION &&
Objects.equals(value.getFunction().getName(), "linear-gradient");
}
public FSLinearGradient getLinearGradient(
PropertyValue value, CssContext cssContext, int boxWidth, int boxHeight) {
if (!isLinearGradient(value)) {
return null;
}
return new FSLinearGradient(this, value.getFunction(), boxWidth, boxHeight, cssContext);
}
/**
* Gets the values of the background properties and combines in a list
* of BackgroundContainer values.
*/
@WebDoc(WebDocLocations.CSS_BACKGROUND_PROPERTIES)
public List getBackgroundImages() {
List images = ((ListValue) valueByName(CSSName.BACKGROUND_IMAGE)).getValues();
List positions = ((ListValue) valueByName(CSSName.BACKGROUND_POSITION)).getValues();
List repeats = ((ListValue) valueByName(CSSName.BACKGROUND_REPEAT)).getValues();
List sizes = ((ListValue) valueByName(CSSName.BACKGROUND_SIZE)).getValues();
assert positions.size() % 2 == 0;
assert sizes.size() % 2 == 0;
List posPairs = new ArrayList<>(positions.size() / 2);
for (int i = 0; i < positions.size(); i += 2) {
posPairs.add(new BackgroundPosition(positions.get(i), positions.get(i + 1)));
}
List sizePairs = new ArrayList<>(sizes.size() / 2);
for (int i = 0; i < sizes.size(); i += 2) {
sizePairs.add(new BackgroundSize(sizes.get(i), sizes.get(i + 1)));
}
List backgrounds = new ArrayList<>(images.size());
for (int i = 0; i < images.size(); i++) {
BackgroundContainer bg = new BackgroundContainer();
PropertyValue img = images.get(i);
if (isLinearGradient(img)) {
bg.type = BackgroundImageType.GRADIENT;
} else if (img.getIdentValue() == IdentValue.NONE) {
bg.type = BackgroundImageType.NONE;
} else {
bg.type = BackgroundImageType.URI;
}
bg.imageGradientOrNone = img;
// If less background-position values are provided than images,
// they must repeat.
bg.backgroundPosition = posPairs.get(i % posPairs.size());
bg.backgroundSize = sizePairs.get(i % sizePairs.size());
bg.backgroundRepeat = repeats.get(i % repeats.size());
backgrounds.add(bg);
}
// Pre-reverse the images, from back to front.
Collections.reverse(backgrounds);
return backgrounds;
}
}